From 21b6f35e4dcdd7725ced8188ce7c71069b007771 Mon Sep 17 00:00:00 2001 From: Stuart Prescott Date: Mon, 1 Sep 2025 05:54:41 +1000 Subject: [PATCH] New upstream version 6.9.2 --- .QT-ENTERPRISE-LICENSE-AGREEMENT | 262 --------- .gitreview | 4 + README.pyside6.md | 2 +- README.pyside6_addons.md | 2 +- README.pyside6_essentials.md | 2 +- README.pyside6_examples.md | 2 +- SECURITY.md | 12 + build_history/blacklist.txt | 9 - build_scripts/config.py | 6 +- build_scripts/main.py | 7 +- build_scripts/platforms/unix.py | 2 +- build_scripts/platforms/windows_desktop.py | 70 ++- build_scripts/qfp_tool.py | 72 ++- build_scripts/utils.py | 6 +- build_scripts/wheel_files.py | 11 +- coin/dependencies.yaml | 2 +- coin/fetch_libclang_arm64.ps1 | 8 + coin/fetch_libclang_arm64.sh | 2 +- coin/instructions/common_environment.yaml | 68 ++- .../execute_desktop_instructions.yaml | 14 +- .../execute_test_instructions.yaml | 2 +- coin/instructions/relocate_pyside.yaml | 2 +- coin/instructions_utils.py | 11 +- coin/module_config.yaml | 3 - create_wheels.py | 4 +- doc/changelogs/changes-6.8.3 | 52 ++ doc/changelogs/changes-6.9.0 | 98 ++++ doc/changelogs/changes-6.9.1 | 64 +++ doc/changelogs/changes-6.9.2 | 59 ++ .../charts/donutbreakdown/donutbreakdown.py | 2 +- examples/charts/dynamicspline/chart.py | 2 +- examples/charts/lineandbar/lineandbar.py | 2 +- .../documentviewer/doc/imageviewer.py.rstinc | 11 + .../documentviewer/documentviewer.pyproject | 1 + .../documentviewer/imageviewer/imageviewer.py | 173 ++++++ .../documentviewer/pdfviewer/pdfviewer.py | 2 +- .../demos/documentviewer/viewerfactory.py | 3 +- .../2d/graphsaudio/GraphsAudio/Main.qml | 50 ++ .../graphs/2d/graphsaudio/GraphsAudio/qmldir | 2 + .../graphs/2d/graphsaudio/doc/graphsaudio.rst | 8 + .../2d/graphsaudio/doc/graphsaudio.webp | Bin 0 -> 12908 bytes .../2d/graphsaudio/graphsaudio.pyproject | 3 + examples/graphs/2d/graphsaudio/main.py | 80 +++ .../3d/widgetgraphgallery/highlightseries.py | 8 +- .../widgetgraphgallery/scatterdatamodifier.py | 115 ++-- .../3d/widgetgraphgallery/scattergraph.py | 25 +- examples/gui/analogclock/main.py | 28 +- examples/multimedia/camera/camera.py | 2 +- .../nanobrowser/ApplicationRoot.qml | 13 +- .../nanobrowser/BrowserWindow.qml | 150 ++++-- .../nanobrowser/DownloadView.qml | 3 +- .../webenginequick/nanobrowser/FindBar.qml | 16 +- .../nanobrowser/quicknanobrowser.py | 2 +- .../webenginewidgets/simplebrowser/browser.py | 7 +- .../webenginewidgets/simplebrowser/main.py | 9 +- .../simplebrowser/ui_webauthdialog.py | 83 +++ .../simplebrowser/webauthdialog.py | 243 +++++++++ .../simplebrowser/webauthdialog.ui | 151 ++++++ .../simplebrowser/webpopupwindow.py | 1 - .../webenginewidgets/simplebrowser/webview.py | 32 +- .../itemviews/stardelegate/starrating.py | 43 +- .../painting/basicdrawing/basicdrawing.py | 78 ++- .../addressbook/addressbook.pyproject | 4 - .../tutorials/addressbook/doc/addressbook.png | Bin 4989 -> 0 bytes .../tutorials/addressbook/doc/addressbook.rst | 9 - .../widgets/tutorials/addressbook/part1.py | 39 -- .../widgets/tutorials/addressbook/part2.py | 146 ----- .../widgets/tutorials/addressbook/part3.py | 215 -------- .../widgets/tutorials/addressbook/part4.py | 272 ---------- .../widgets/tutorials/addressbook/part5.py | 331 ------------ .../widgets/tutorials/addressbook/part6.py | 394 -------------- .../widgets/tutorials/addressbook/part7.py | 445 --------------- examples/widgets/tutorials/cannon/t11.py | 13 +- examples/widgets/tutorials/cannon/t12.py | 13 +- examples/widgets/tutorials/cannon/t13.py | 13 +- examples/widgets/tutorials/cannon/t14.py | 15 +- examples/xml/dombookmarks/dombookmarks.py | 11 +- requirements-coin.txt | 4 +- requirements.txt | 8 +- sources/pyside-tools/deploy.py | 27 +- .../deploy_lib/android/android_utilities.py | 15 +- sources/pyside-tools/deploy_lib/config.py | 142 ++--- sources/pyside-tools/deploy_lib/default.spec | 59 +- .../deploy_lib/dependency_util.py | 30 +- sources/pyside-tools/metaobjectdump.py | 4 +- sources/pyside-tools/project.py | 137 ++--- sources/pyside-tools/project_lib/__init__.py | 11 +- .../pyside-tools/project_lib/newproject.py | 118 ++-- .../pyside-tools/project_lib/project_data.py | 63 ++- .../project_lib/pyproject_json.py | 58 ++ .../project_lib/pyproject_parse_result.py | 10 + .../project_lib/pyproject_toml.py | 235 ++++++++ sources/pyside-tools/project_lib/utils.py | 105 +++- sources/pyside-tools/pyside_tool.py | 27 +- sources/pyside-tools/requirements-android.txt | 1 + sources/pyside6/.cmake.conf | 4 +- sources/pyside6/CMakeLists.txt | 4 + sources/pyside6/PySide6/CMakeLists.txt | 2 - .../Qt3DAnimation/typesystem_3danimation.xml | 2 +- sources/pyside6/PySide6/QtAsyncio/events.py | 4 +- sources/pyside6/PySide6/QtAsyncio/tasks.py | 7 +- sources/pyside6/PySide6/QtCore/CMakeLists.txt | 1 + .../PySide6/QtCore/glue/core_snippets.cpp | 15 +- .../PySide6/QtCore/typesystem_core_common.xml | 202 +++++-- .../pyside6/PySide6/QtGraphs/CMakeLists.txt | 1 + .../PySide6/QtGraphs/typesystem_graphs.xml | 4 +- sources/pyside6/PySide6/QtGui/CMakeLists.txt | 2 + .../PySide6/QtGui/typesystem_gui_common.xml | 331 +++++++++--- .../QtMultimedia/typesystem_multimedia.xml | 3 +- .../PySide6/QtNetwork/typesystem_network.xml | 6 +- .../PySide6/QtNetworkAuth/CMakeLists.txt | 1 + .../QtNetworkAuth/typesystem_networkauth.xml | 2 + .../PySide6/QtOpenGL/typesystem_opengl.xml | 1 - .../typesystem_opengl_modifications1_0.xml | 4 +- .../typesystem_printsupport_common.xml | 2 - .../pyside6/PySide6/QtQml/typesystem_qml.xml | 1 + .../PySide6/QtQuick/typesystem_quick.xml | 1 + .../PySide6/QtRemoteObjects/CMakeLists.txt | 9 +- .../typesystem_remoteobjects.xml | 17 +- .../QtSerialBus/typesystem_serialbus.xml | 2 + .../PySide6/QtTest/typesystem_test.xml | 12 +- .../PySide6/QtWebEngineCore/CMakeLists.txt | 1 + .../typesystem_webenginecore.xml | 2 + .../PySide6/QtWebEngineQuick/CMakeLists.txt | 1 + .../typesystem_webenginequick.xml | 2 + .../QtWidgets/typesystem_widgets_common.xml | 181 +++---- sources/pyside6/PySide6/__init__.py.in | 11 +- sources/pyside6/PySide6/_config.py.in | 4 +- sources/pyside6/PySide6/doc/qtcore.rst | 23 + sources/pyside6/PySide6/doc/qtqml.rst | 8 +- sources/pyside6/PySide6/glue/qtcore.cpp | 100 +++- sources/pyside6/PySide6/glue/qtgui.cpp | 234 +++++++- sources/pyside6/PySide6/glue/qtnetwork.cpp | 2 +- sources/pyside6/PySide6/glue/qtquick.cpp | 2 +- .../pyside6/PySide6/glue/qtremoteobjects.cpp | 31 ++ sources/pyside6/PySide6/glue/qttest.cpp | 6 + sources/pyside6/PySide6/glue/qtwidgets.cpp | 10 + sources/pyside6/PySide6/templates/common.xml | 24 + .../pyside6/PySide6/templates/core_common.xml | 78 +-- .../templates/datavisualization_common.xml | 4 +- .../pyside6/PySide6/templates/gui_common.xml | 212 +------- .../PySide6/templates/opengl_common.xml | 30 -- .../PySide6/templates/widgets_common.xml | 35 +- .../pyside6/cmake/Macros/PySideModules.cmake | 1 + sources/pyside6/cmake/PySideHelpers.cmake | 8 - sources/pyside6/cmake/PySideSetup.cmake | 16 + .../doc/building_from_source/linux.rst | 2 +- .../doc/building_from_source/macOS.rst | 2 +- .../doc/building_from_source/windows.rst | 11 +- .../deployment/deployment-pyside6-deploy.rst | 4 +- .../doc/developer/add_port_example.rst | 1 + sources/pyside6/doc/developer/extras.rst | 11 +- .../pyside6/doc/developer/fix_type_hints.rst | 177 ++++++ sources/pyside6/doc/developer/index.rst | 2 + sources/pyside6/doc/developer/limited_api.rst | 31 -- .../pyside6/doc/developer/remoteobjects.md | 162 ++++++ sources/pyside6/doc/faq/typesoffiles.rst | 27 +- sources/pyside6/doc/tools/pyside-project.rst | 112 +++- .../basictutorial/signals_and_slots.rst | 19 + .../datavisualize3/main_window.py | 1 - .../datavisualize5/main_widget.py | 7 +- .../datavisualize6/main_window.py | 3 +- .../doc/tutorials/expenses/main_snake_prop.py | 2 +- .../tutorials/expenses/steps/01-expenses.py | 1 + .../tutorials/finance_manager/part3/part3.md | 10 +- .../modelviewprogramming/qlistview-dnd.py | 3 +- .../modelviewprogramming/stringlistmodel.py | 4 +- .../portingguide/chapter1/createdb.py | 8 +- .../portingguide/chapter2/bookdelegate.py | 10 +- .../portingguide/chapter2/createdb.py | 8 +- .../tutorials/portingguide/chapter2/main.py | 1 - .../portingguide/chapter3/bookdelegate-old.py | 10 +- .../portingguide/chapter3/bookdelegate.py | 20 +- .../portingguide/chapter3/bookwindow.py | 26 +- .../portingguide/chapter3/chapter3.pyproject | 3 + .../portingguide/chapter3/createdb.py | 8 +- .../tutorials/portingguide/chapter3/main.py | 2 +- .../portingguide/chapter3/rc_books.py | 88 +++ .../portingguide/chapter3/ui_bookwindow.py | 142 +++++ .../tutorials/portingguide/hello_world_ex.py | 5 +- sources/pyside6/libpyside/CMakeLists.txt | 2 + sources/pyside6/libpyside/class_property.cpp | 17 +- .../pyside6/libpyside/dynamicqmetaobject.cpp | 19 +- .../pyside6/libpyside/dynamicqmetaobject.h | 4 +- sources/pyside6/libpyside/dynamicslot.cpp | 65 +-- sources/pyside6/libpyside/dynamicslot_p.h | 4 +- sources/pyside6/libpyside/feature_select.cpp | 9 +- sources/pyside6/libpyside/pyside.cpp | 34 +- sources/pyside6/libpyside/pyside_numpy.h | 6 +- .../libpyside/pysideclassdecorator_p.h | 2 +- sources/pyside6/libpyside/pysideclassinfo.cpp | 3 +- sources/pyside6/libpyside/pysideclassinfo.h | 4 +- sources/pyside6/libpyside/pysideclassinfo_p.h | 2 +- sources/pyside6/libpyside/pysidelogging_p.h | 2 +- .../pyside6/libpyside/pysidemetafunction.cpp | 15 +- .../pyside6/libpyside/pysidemetafunction.h | 2 +- sources/pyside6/libpyside/pysideproperty.cpp | 51 +- sources/pyside6/libpyside/pysideproperty.h | 2 +- sources/pyside6/libpyside/pysideproperty_p.h | 4 +- sources/pyside6/libpyside/pysideqenum.cpp | 10 +- sources/pyside6/libpyside/pysideqenum.h | 3 + sources/pyside6/libpyside/pysideqhash.h | 2 +- sources/pyside6/libpyside/pysideqmetatype.h | 2 +- .../pyside6/libpyside/pysideqslotobject_p.h | 2 +- sources/pyside6/libpyside/pysidesignal.cpp | 318 +++++------ sources/pyside6/libpyside/pysidesignal.h | 7 +- sources/pyside6/libpyside/pysidesignal_p.h | 10 +- sources/pyside6/libpyside/pysideslot.cpp | 14 +- sources/pyside6/libpyside/pysideutils.h | 2 + sources/pyside6/libpyside/pysideweakref.cpp | 4 +- sources/pyside6/libpyside/pysideweakref.h | 6 +- sources/pyside6/libpyside/qobjectconnect.cpp | 28 +- sources/pyside6/libpyside/qobjectconnect.h | 2 +- sources/pyside6/libpyside/signalmanager.cpp | 98 +++- sources/pyside6/libpyside/signalmanager.h | 11 +- sources/pyside6/libpysideqml/CMakeLists.txt | 2 + sources/pyside6/libpysideqml/pysideqml.cpp | 4 +- .../libpysideqml/pysideqmlattached.cpp | 7 +- .../libpysideqml/pysideqmlextended.cpp | 7 +- .../pyside6/libpysideqml/pysideqmlforeign.cpp | 8 +- .../libpysideqml/pysideqmllistproperty.cpp | 18 +- .../libpysideqml/pysideqmlmetacallerror.cpp | 8 +- .../libpysideqml/pysideqmlnamedelement.cpp | 2 +- .../libpysideqml/pysideqmlregistertype.cpp | 13 +- .../libpysideqml/pysideqmlregistertype_p.h | 2 +- .../libpysideqml/pysideqmltypeinfo.cpp | 4 +- .../libpysideqml/pysideqmltypeinfo_p.h | 4 +- .../libpysideqml/pysideqmluncreatable.cpp | 4 +- .../libpysideqml/pysideqmluncreatable.h | 2 +- .../libpysideremoteobjects/CMakeLists.txt | 88 +++ .../pysidecapsulemethod.cpp | 230 ++++++++ .../pysidecapsulemethod_p.h | 87 +++ .../pysidedynamicclass.cpp | 506 ++++++++++++++++++ .../pysidedynamicclass_p.h | 15 + .../pysidedynamiccommon.cpp | 126 +++++ .../pysidedynamiccommon_p.h | 84 +++ .../pysidedynamicenum.cpp | 158 ++++++ .../pysidedynamicenum_p.h | 15 + .../pysidedynamicpod.cpp | 260 +++++++++ .../pysidedynamicpod_p.h | 15 + .../pysideremoteobjects.h | 16 + .../pysiderephandler.cpp | 463 ++++++++++++++++ .../pysiderephandler_p.h | 35 ++ .../pyside6/plugins/designer/CMakeLists.txt | 2 +- .../designer/designercustomwidgets.cpp | 18 +- .../plugins/designer/designercustomwidgets.h | 2 +- .../pyside6/plugins/uitools/customwidget.cpp | 6 + .../pyside6/plugins/uitools/customwidget.h | 4 +- .../pyside6/plugins/uitools/customwidgets.h | 4 +- sources/pyside6/qtexampleicons/CMakeLists.txt | 2 +- .../pyside6/tests/QtBluetooth/localdevice.py | 6 +- .../QtBluetooth/lowenergy_characteristics.py | 10 +- sources/pyside6/tests/QtCore/CMakeLists.txt | 5 + .../pyside6/tests/QtCore/attr_cache_py3k.py | 4 +- sources/pyside6/tests/QtCore/bug_1313.py | 2 +- sources/pyside6/tests/QtCore/bug_324.py | 2 +- sources/pyside6/tests/QtCore/bug_332.py | 3 +- sources/pyside6/tests/QtCore/bug_706.py | 2 +- sources/pyside6/tests/QtCore/bug_835.py | 2 +- .../tests/QtCore/destroysignal_test.py | 2 +- .../errormessages_with_features_test.py | 2 +- sources/pyside6/tests/QtCore/max_signals.py | 4 +- .../tests/QtCore/multiple_feature_test.py | 4 +- .../pyside6/tests/QtCore/python_conversion.py | 6 +- .../tests/QtCore/qanimationgroup_test.py | 5 +- .../QtCore/qbytearray_operator_iadd_test.py | 4 +- .../QtCore/qiodevice_buffered_read_test.py | 11 +- sources/pyside6/tests/QtCore/qiopipe_test.py | 12 +- sources/pyside6/tests/QtCore/qlocale_test.py | 2 +- .../pyside6/tests/QtCore/qlockfile_test.py | 2 +- .../tests/QtCore/qmessage_logger_test.py | 74 +++ .../QtCore/qobject_children_segfault_test.py | 4 +- .../tests/QtCore/qobject_destructor.py | 9 +- .../tests/QtCore/qobject_inherits_test.py | 2 +- .../tests/QtCore/qobject_parent_test.py | 48 +- .../tests/QtCore/qobject_property_test.py | 34 ++ .../tests/QtCore/qproperty_decorator.py | 6 +- .../tests/QtCore/qrandomgenerator_test.py | 2 +- .../tests/QtCore/qsharedmemory_client.py | 9 +- .../tests/QtCore/snake_prop_feature_test.py | 2 +- .../pyside6/tests/QtCore/versioninfo_test.py | 2 +- sources/pyside6/tests/QtGui/CMakeLists.txt | 2 +- sources/pyside6/tests/QtGui/bug_660.py | 3 +- sources/pyside6/tests/QtGui/bug_PYSIDE-344.py | 3 +- .../pyside6/tests/QtGui/event_filter_test.py | 7 +- sources/pyside6/tests/QtGui/qfont_test.py | 5 + sources/pyside6/tests/QtGui/qicon_test.py | 2 +- .../pyside6/tests/QtGui/qpaintengine_test.py | 86 +++ .../tests/QtGui/qpixmap_constructor.py | 6 +- sources/pyside6/tests/QtHelp/help_test.py | 2 +- sources/pyside6/tests/QtLocation/location.py | 4 +- .../QtMultimediaWidgets/qmultimediawidgets.py | 12 +- .../tests/QtPositioning/positioning.py | 4 +- sources/pyside6/tests/QtQml/CMakeLists.txt | 1 + .../pyside6/tests/QtQml/groupedproperty.py | 8 +- .../tests/QtQml/qquickitem_grabToImage.py | 12 +- .../pyside6/tests/QtQml/registerattached.py | 12 +- .../pyside6/tests/QtQml/registerextended.py | 12 +- .../tests/QtQml/registerparserstatus.py | 10 +- .../pyside6/tests/QtQml/registerqmlfile.py | 10 +- .../tests/QtQml/registersingletontype.py | 2 +- sources/pyside6/tests/QtQml/signal_types.py | 18 +- .../tst_quicktestmainwithsetup.py | 10 +- .../tests/QtRemoteObjects/CMakeLists.txt | 12 +- .../cpp_interop/CMakeLists.txt | 25 + .../cpp_interop/cpp_interop.cpp | 128 +++++ .../cpp_interop/cpp_interop_test.py | 197 +++++++ .../QtRemoteObjects/dynamic_types_test.py | 97 ++++ .../tests/QtRemoteObjects/integration_test.py | 369 +++++++++++++ .../tests/QtRemoteObjects/repfile_test.py | 65 +++ .../pyside6/tests/QtRemoteObjects/simple.rep | 7 + .../tests/QtRemoteObjects/test_shared.py | 126 +++++ .../pyside6/tests/QtSerialBus/CMakeLists.txt | 2 +- .../tests/QtSerialBus/QtSerialBus.pyproject | 3 + .../pyside6/tests/QtSerialBus/serialbus.py | 44 ++ sources/pyside6/tests/QtSerialBus/test.dbc | 10 + sources/pyside6/tests/QtTest/CMakeLists.txt | 3 +- .../pyside6/tests/QtTest/touchevent_test.py | 29 +- .../pyside6/tests/QtUiTools/CMakeLists.txt | 1 + sources/pyside6/tests/QtUiTools/bug_913.py | 2 +- sources/pyside6/tests/QtUiTools/bug_958.py | 3 +- .../tests/QtUiTools/loadUiType_test.py | 3 + .../pyside6/tests/QtWidgets/CMakeLists.txt | 6 +- .../pyside6/tests/QtWidgets/action_clear.py | 12 +- sources/pyside6/tests/QtWidgets/bug_1002.py | 6 +- sources/pyside6/tests/QtWidgets/bug_1006.py | 11 +- sources/pyside6/tests/QtWidgets/bug_1048.py | 4 +- sources/pyside6/tests/QtWidgets/bug_243.py | 6 +- sources/pyside6/tests/QtWidgets/bug_338.py | 2 +- sources/pyside6/tests/QtWidgets/bug_433.py | 2 +- sources/pyside6/tests/QtWidgets/bug_525.py | 2 +- sources/pyside6/tests/QtWidgets/bug_546.py | 2 +- sources/pyside6/tests/QtWidgets/bug_576.py | 9 +- sources/pyside6/tests/QtWidgets/bug_640.py | 3 +- sources/pyside6/tests/QtWidgets/bug_653.py | 2 +- sources/pyside6/tests/QtWidgets/bug_668.py | 2 +- sources/pyside6/tests/QtWidgets/bug_674.py | 4 +- sources/pyside6/tests/QtWidgets/bug_693.py | 2 +- sources/pyside6/tests/QtWidgets/bug_750.py | 18 +- sources/pyside6/tests/QtWidgets/bug_862.py | 2 +- sources/pyside6/tests/QtWidgets/bug_919.py | 9 +- sources/pyside6/tests/QtWidgets/bug_941.py | 13 +- sources/pyside6/tests/QtWidgets/bug_967.py | 2 +- sources/pyside6/tests/QtWidgets/bug_988.py | 2 +- sources/pyside6/tests/QtWidgets/bug_998.py | 2 +- sources/pyside6/tests/QtWidgets/pyside3069.py | 51 ++ .../pyside_reload_test.py | 44 +- .../pyside6/tests/QtWidgets/qapp_issue_585.py | 10 +- sources/pyside6/tests/QtWidgets/qapp_test.py | 4 +- .../pyside6/tests/QtWidgets/qdialog_test.py | 26 +- .../tests/QtWidgets/qfontdialog_test.py | 39 +- .../QtWidgets/qgraphicsobjectreimpl_test.py | 9 + .../tests/QtWidgets/qinputdialog_get_test.py | 33 +- .../tests/QtWidgets/qlayout_ref_test.py | 20 +- .../pyside6/tests/QtWidgets/qlayout_test.py | 12 +- .../tests/QtWidgets/qlistwidgetitem_test.py | 2 +- .../tests/QtWidgets/qmainwindow_test.py | 18 +- sources/pyside6/tests/QtWidgets/qmenu_test.py | 18 +- .../pyside6/tests/QtWidgets/qpicture_test.py | 16 +- .../pyside6/tests/QtWidgets/qstyle_test.py | 4 +- .../pyside6/tests/QtWidgets/qtextedit_test.py | 2 +- .../tests/QtWidgets/qtreewidget_test.py | 2 +- sources/pyside6/tests/init_paths.py | 4 +- sources/pyside6/tests/manually/sizebench.py | 70 ++- .../pyside6/tests/pysidetest/CMakeLists.txt | 1 + sources/pyside6/tests/pysidetest/bug_1016.py | 1 - sources/pyside6/tests/pysidetest/enum_test.py | 30 +- .../pyside6/tests/pysidetest/iterable_test.py | 4 +- .../tests/pysidetest/mypy_correctness_test.py | 23 +- sources/pyside6/tests/pysidetest/notify_id.py | 4 +- .../tests/pysidetest/signal_slot_warning.py | 4 +- .../pysidetest/signal_tp_descr_get_test.py | 2 +- .../signalemissionfrompython_test.py | 13 + .../pyside6/tests/pysidetest/testobject.cpp | 5 + sources/pyside6/tests/pysidetest/testobject.h | 3 + .../pyside6/tests/registry/existence_test.py | 16 +- sources/pyside6/tests/signals/CMakeLists.txt | 2 + .../pyside6/tests/signals/disconnect_test.py | 87 ++- .../signals/nonqobject_receivers_test.py | 71 +++ sources/pyside6/tests/signals/ref03_test.py | 6 +- .../signals/slot_reference_count_test.py | 12 +- .../metaobjectdump/test_metaobjectdump.py | 4 +- .../test_pyside6_android_deploy.py | 2 +- .../pyside6-deploy/test_pyside6_deploy.py | 118 ++-- .../Python/{.pyproject => Drumpad.pyproject} | 0 .../example_drumpad/Python/pyproject.toml | 5 + .../example_project/example_project.pyproject | 3 + .../example_project/folder/label_in_folder.py | 9 + .../pyside6-project/example_project/main.py | 20 + .../example_project/mainwindow.py | 22 + .../example_project/pyproject.toml | 5 + .../example_project/subproject/pyproject.toml | 5 + .../subproject/subproject.pyproject | 3 + .../subproject/subproject_button.py | 17 + .../existing_pyproject_toml.pyproject | 3 + .../expected_pyproject.toml | 22 + .../existing_pyproject_toml/main.py | 2 + .../existing_pyproject_toml/pyproject.toml | 20 + .../existing_pyproject_toml/zzz.py | 2 + .../invalid_pyproject.pyproject | 3 + .../pyside6-project/invalid_pyproject/main.py | 0 .../invalid_pyproject/pyproject.toml | 2 + .../valid_pyproject.pyproject | 3 + .../multiple_pyproject/common_file.py | 0 .../expected_pyproject.toml | 5 + .../multiple_pyproject/file1.py | 0 .../multiple_pyproject/file2.py | 0 .../multiple_pyproject/project1.pyproject | 3 + .../multiple_pyproject/project2.pyproject | 3 + .../pyside6-project/test_pyside6_project.py | 434 ++++++++++++--- .../tools/pyside6-qml/test_pyside6_qml.py | 4 +- .../tests/util/helper/basicpyslotcase.py | 2 +- .../pyside6/tests/util/helper/docmodifier.py | 20 +- .../tests/util/helper/usesqapplication.py | 4 +- sources/pyside6/tests/util/processtimer.py | 55 -- .../pyside6/tests/util/test_processtimer.py | 74 --- sources/shiboken6/.cmake.conf | 4 +- sources/shiboken6/ApiExtractor/CMakeLists.txt | 1 + .../ApiExtractor/abstractmetaargument.cpp | 4 +- .../ApiExtractor/abstractmetaargument.h | 2 +- .../ApiExtractor/abstractmetabuilder.cpp | 286 +++++----- .../ApiExtractor/abstractmetabuilder.h | 3 +- .../ApiExtractor/abstractmetabuilder_p.h | 10 +- .../ApiExtractor/abstractmetaenum.cpp | 2 +- .../shiboken6/ApiExtractor/abstractmetaenum.h | 4 +- .../ApiExtractor/abstractmetafield.cpp | 2 +- .../ApiExtractor/abstractmetafield.h | 2 +- .../ApiExtractor/abstractmetafunction.cpp | 187 ++++--- .../ApiExtractor/abstractmetafunction.h | 53 +- .../ApiExtractor/abstractmetalang.cpp | 355 +++++++----- .../shiboken6/ApiExtractor/abstractmetalang.h | 37 +- .../ApiExtractor/abstractmetalang_enums.h | 19 +- .../ApiExtractor/abstractmetalang_typedefs.h | 2 +- .../ApiExtractor/abstractmetatype.cpp | 74 ++- .../shiboken6/ApiExtractor/abstractmetatype.h | 21 +- .../shiboken6/ApiExtractor/addedfunction.cpp | 4 +- .../shiboken6/ApiExtractor/addedfunction.h | 4 +- .../shiboken6/ApiExtractor/addedfunction_p.h | 8 +- .../ApiExtractor/anystringview_helpers.cpp | 6 +- .../ApiExtractor/anystringview_helpers.h | 4 +- .../shiboken6/ApiExtractor/apiextractor.cpp | 38 +- sources/shiboken6/ApiExtractor/apiextractor.h | 4 +- .../ApiExtractor/apiextractorflags.h | 2 +- .../ApiExtractor/apiextractorresult.h | 3 +- .../shiboken6/ApiExtractor/arraytypeentry.h | 4 +- .../ApiExtractor/clangparser/clangbuilder.cpp | 48 +- .../ApiExtractor/clangparser/clangbuilder.h | 5 + .../clangparser/clangdebugutils.cpp | 4 +- .../ApiExtractor/clangparser/clangparser.cpp | 47 +- .../ApiExtractor/clangparser/clangparser.h | 8 +- .../ApiExtractor/clangparser/clangutils.cpp | 8 +- .../ApiExtractor/clangparser/clangutils.h | 8 +- .../clangparser/compilersupport.cpp | 85 ++- .../clangparser/compilersupport.h | 10 +- .../ApiExtractor/classdocumentation.cpp | 10 +- .../ApiExtractor/classdocumentation.h | 2 +- sources/shiboken6/ApiExtractor/codesnip.cpp | 101 +++- sources/shiboken6/ApiExtractor/codesnip.h | 69 +-- .../ApiExtractor/codesniphelpers.cpp | 2 +- .../shiboken6/ApiExtractor/codesniphelpers.h | 2 +- .../shiboken6/ApiExtractor/complextypeentry.h | 22 +- .../ApiExtractor/conditionalstreamreader.cpp | 4 +- .../ApiExtractor/conditionalstreamreader.h | 2 +- .../ApiExtractor/configurabletypeentry.h | 4 +- sources/shiboken6/ApiExtractor/cpptypeentry.h | 59 ++ .../shiboken6/ApiExtractor/customconversion.h | 4 +- .../ApiExtractor/customconversion_typedefs.h | 2 +- .../shiboken6/ApiExtractor/customtypenentry.h | 8 +- .../shiboken6/ApiExtractor/debughelpers_p.h | 2 +- sources/shiboken6/ApiExtractor/dependency.h | 2 +- sources/shiboken6/ApiExtractor/docparser.cpp | 12 +- sources/shiboken6/ApiExtractor/docparser.h | 2 +- .../shiboken6/ApiExtractor/documentation.cpp | 2 +- .../shiboken6/ApiExtractor/documentation.h | 4 +- .../ApiExtractor/documentation_enums.h | 8 +- sources/shiboken6/ApiExtractor/dotview.cpp | 10 +- sources/shiboken6/ApiExtractor/dotview.h | 2 +- .../shiboken6/ApiExtractor/doxygenparser.cpp | 4 +- sources/shiboken6/ApiExtractor/exception.h | 2 +- sources/shiboken6/ApiExtractor/filecache.cpp | 142 +++++ sources/shiboken6/ApiExtractor/filecache.h | 53 ++ sources/shiboken6/ApiExtractor/fileout.cpp | 6 +- .../shiboken6/ApiExtractor/flagstypeentry.h | 4 +- sources/shiboken6/ApiExtractor/graph.h | 12 +- sources/shiboken6/ApiExtractor/header_paths.h | 4 +- sources/shiboken6/ApiExtractor/include.cpp | 8 +- sources/shiboken6/ApiExtractor/include.h | 10 +- sources/shiboken6/ApiExtractor/messages.cpp | 89 ++- sources/shiboken6/ApiExtractor/messages.h | 16 +- .../shiboken6/ApiExtractor/modifications.cpp | 120 ++++- .../shiboken6/ApiExtractor/modifications.h | 49 +- .../ApiExtractor/modifications_typedefs.h | 2 +- .../shiboken6/ApiExtractor/optionsparser.cpp | 20 +- .../shiboken6/ApiExtractor/optionsparser.h | 4 +- .../ApiExtractor/parser/codemodel.cpp | 28 +- .../shiboken6/ApiExtractor/parser/codemodel.h | 15 +- .../ApiExtractor/parser/codemodel_fwd.h | 2 +- .../ApiExtractor/parser/enumvalue.cpp | 6 +- .../shiboken6/ApiExtractor/parser/enumvalue.h | 2 +- .../ApiExtractor/parser/typeinfo.cpp | 44 +- .../shiboken6/ApiExtractor/parser/typeinfo.h | 13 +- .../ApiExtractor/predefined_templates.cpp | 24 +- .../ApiExtractor/predefined_templates.h | 4 +- .../ApiExtractor/primitivetypeentry.h | 10 +- .../shiboken6/ApiExtractor/propertyspec.cpp | 6 +- sources/shiboken6/ApiExtractor/propertyspec.h | 4 +- .../ApiExtractor/pymethoddefentry.cpp | 2 +- .../shiboken6/ApiExtractor/pymethoddefentry.h | 4 +- .../shiboken6/ApiExtractor/pythontypeentry.h | 2 +- sources/shiboken6/ApiExtractor/qtcompat.h | 2 +- .../shiboken6/ApiExtractor/qtdocparser.cpp | 12 +- .../shiboken6/ApiExtractor/reporthandler.cpp | 62 ++- .../shiboken6/ApiExtractor/reporthandler.h | 7 +- .../ApiExtractor/smartpointertypeentry.h | 6 +- .../shiboken6/ApiExtractor/sourcelocation.cpp | 4 +- .../shiboken6/ApiExtractor/sourcelocation.h | 2 +- .../tests/testabstractmetaclass.cpp | 85 ++- .../tests/testabstractmetaclass.h | 4 +- .../tests/testabstractmetatype.cpp | 2 +- .../ApiExtractor/tests/testabstractmetatype.h | 2 +- .../ApiExtractor/tests/testaddfunction.cpp | 2 +- .../ApiExtractor/tests/testaddfunction.h | 3 +- .../ApiExtractor/tests/testarrayargument.cpp | 2 +- .../ApiExtractor/tests/testarrayargument.h | 3 +- .../ApiExtractor/tests/testcodeinjection.cpp | 7 +- .../ApiExtractor/tests/testcodeinjection.h | 2 +- .../ApiExtractor/tests/testcontainer.cpp | 4 +- .../ApiExtractor/tests/testcontainer.h | 2 +- .../tests/testconversionoperator.cpp | 2 +- .../tests/testconversionoperator.h | 3 +- .../tests/testconversionruletag.cpp | 7 +- .../tests/testconversionruletag.h | 2 +- .../tests/testctorinformation.cpp | 4 +- .../ApiExtractor/tests/testctorinformation.h | 2 +- .../tests/testdroptypeentries.cpp | 2 +- .../ApiExtractor/tests/testdroptypeentries.h | 2 +- .../tests/testdtorinformation.cpp | 3 +- .../ApiExtractor/tests/testdtorinformation.h | 2 +- .../shiboken6/ApiExtractor/tests/testenum.cpp | 2 +- .../shiboken6/ApiExtractor/tests/testenum.h | 2 +- .../ApiExtractor/tests/testextrainclude.cpp | 4 +- .../ApiExtractor/tests/testextrainclude.h | 2 +- .../ApiExtractor/tests/testfunctiontag.cpp | 2 +- .../ApiExtractor/tests/testfunctiontag.h | 2 +- .../tests/testimplicitconversions.cpp | 4 +- .../tests/testimplicitconversions.h | 2 +- .../ApiExtractor/tests/testinserttemplate.cpp | 4 +- .../ApiExtractor/tests/testinserttemplate.h | 2 +- .../tests/testmodifydocumentation.cpp | 6 +- .../tests/testmodifydocumentation.h | 2 +- .../ApiExtractor/tests/testmodifyfunction.cpp | 2 +- .../ApiExtractor/tests/testmodifyfunction.h | 2 +- .../tests/testmultipleinheritance.cpp | 4 +- .../tests/testmultipleinheritance.h | 2 +- .../ApiExtractor/tests/testnamespace.cpp | 2 +- .../ApiExtractor/tests/testnamespace.h | 2 +- .../ApiExtractor/tests/testnestedtypes.cpp | 2 +- .../ApiExtractor/tests/testnestedtypes.h | 2 +- .../tests/testnumericaltypedef.cpp | 4 +- .../ApiExtractor/tests/testnumericaltypedef.h | 2 +- .../tests/testprimitivetypetag.cpp | 2 +- .../ApiExtractor/tests/testprimitivetypetag.h | 2 +- .../ApiExtractor/tests/testrefcounttag.cpp | 2 +- .../ApiExtractor/tests/testrefcounttag.h | 2 +- .../tests/testreferencetopointer.cpp | 4 +- .../tests/testreferencetopointer.h | 2 +- .../ApiExtractor/tests/testremovefield.cpp | 4 +- .../ApiExtractor/tests/testremovefield.h | 2 +- .../ApiExtractor/tests/testremoveimplconv.cpp | 4 +- .../ApiExtractor/tests/testremoveimplconv.h | 2 +- .../tests/testremoveoperatormethod.cpp | 2 +- .../tests/testremoveoperatormethod.h | 2 +- .../ApiExtractor/tests/testresolvetype.cpp | 2 +- .../ApiExtractor/tests/testresolvetype.h | 2 +- .../tests/testreverseoperators.cpp | 4 +- .../ApiExtractor/tests/testreverseoperators.h | 2 +- .../ApiExtractor/tests/testtemplates.cpp | 7 +- .../ApiExtractor/tests/testtemplates.h | 2 +- .../ApiExtractor/tests/testtoposort.cpp | 4 +- .../ApiExtractor/tests/testtoposort.h | 2 +- .../ApiExtractor/tests/testtyperevision.cpp | 2 +- .../ApiExtractor/tests/testtyperevision.h | 2 +- .../shiboken6/ApiExtractor/tests/testutil.h | 16 +- .../tests/testvaluetypedefaultctortag.cpp | 4 +- .../tests/testvaluetypedefaultctortag.h | 2 +- .../ApiExtractor/tests/testvoidarg.cpp | 4 +- .../ApiExtractor/tests/testvoidarg.h | 3 +- sources/shiboken6/ApiExtractor/textstream.h | 8 +- .../shiboken6/ApiExtractor/typedatabase.cpp | 155 ++++-- sources/shiboken6/ApiExtractor/typedatabase.h | 22 +- .../shiboken6/ApiExtractor/typedatabase_p.h | 12 +- .../ApiExtractor/typedatabase_typedefs.h | 6 +- sources/shiboken6/ApiExtractor/typedefentry.h | 2 +- sources/shiboken6/ApiExtractor/typeparser.cpp | 8 +- sources/shiboken6/ApiExtractor/typeparser.h | 2 +- sources/shiboken6/ApiExtractor/typesystem.cpp | 361 ++++++++----- sources/shiboken6/ApiExtractor/typesystem.h | 10 +- .../shiboken6/ApiExtractor/typesystem_enums.h | 18 + .../ApiExtractor/typesystem_typedefs.h | 5 +- .../ApiExtractor/typesystemparser.cpp | 334 +++++++----- .../ApiExtractor/typesystemparser_p.h | 26 +- .../shiboken6/ApiExtractor/voidtypeentry.h | 6 +- sources/shiboken6/ApiExtractor/xmlutils.h | 2 +- .../ApiExtractor/xmlutils_libxslt.cpp | 10 +- .../shiboken6/ApiExtractor/xmlutils_libxslt.h | 2 +- sources/shiboken6/ApiExtractor/xmlutils_qt.h | 2 +- sources/shiboken6/cmake/ShibokenHelpers.cmake | 4 +- sources/shiboken6/doc/index.rst | 2 +- sources/shiboken6/doc/scripts/patch_qhp.py | 4 +- sources/shiboken6/doc/typesystem.rst | 2 + .../shiboken6/doc/typesystem_arguments.rst | 34 +- .../doc/typesystem_codeinjection.rst | 4 +- .../shiboken6/doc/typesystem_converters.rst | 36 +- .../doc/typesystem_documentation.rst | 5 + .../doc/typesystem_manipulating_objects.rst | 15 +- .../shiboken6/doc/typesystem_objectvalue.rst | 61 +++ .../shiboken6/doc/typesystem_overloads.rst | 38 ++ .../doc/typesystem_specialfunctions.rst | 6 +- .../doc/typesystem_specifying_types.rst | 69 ++- .../shiboken6/doc/typesystem_templates.rst | 7 +- .../shiboken6/doc/typesystem_variables.rst | 2 + sources/shiboken6/generator/CMakeLists.txt | 1 + sources/shiboken6/generator/defaultvalue.cpp | 2 +- sources/shiboken6/generator/defaultvalue.h | 2 +- sources/shiboken6/generator/generator.cpp | 36 +- sources/shiboken6/generator/generator.h | 7 +- .../shiboken6/generator/generatorcontext.cpp | 2 +- .../shiboken6/generator/generatorcontext.h | 2 +- sources/shiboken6/generator/main.cpp | 21 +- .../generator/qtdoc/qtdocgenerator.cpp | 45 +- .../generator/qtdoc/qtdocgenerator.h | 18 +- .../generator/qtdoc/qtxmltosphinx.cpp | 130 ++--- .../shiboken6/generator/qtdoc/qtxmltosphinx.h | 11 +- .../generator/qtdoc/qtxmltosphinxinterface.h | 2 +- sources/shiboken6/generator/qtdoc/rstformat.h | 8 +- .../generator/shiboken/cppgenerator.cpp | 416 ++++++++------ .../generator/shiboken/cppgenerator.h | 13 +- .../shiboken/cppgenerator_container.cpp | 2 +- .../shiboken/cppgenerator_smartpointer.cpp | 8 +- .../shiboken6/generator/shiboken/ctypenames.h | 2 +- .../generator/shiboken/generatorargument.cpp | 4 +- .../generator/shiboken/generatorargument.h | 2 +- .../generator/shiboken/generatorstrings.h | 2 +- .../generator/shiboken/headergenerator.cpp | 237 ++++---- .../generator/shiboken/headergenerator.h | 17 +- .../generator/shiboken/overloaddata.cpp | 24 +- .../generator/shiboken/overloaddata.h | 4 +- .../generator/shiboken/overridecacheentry.cpp | 28 + .../generator/shiboken/overridecacheentry.h | 42 ++ .../generator/shiboken/pytypenames.h | 2 +- .../generator/shiboken/shibokengenerator.cpp | 310 ++++++++++- .../generator/shiboken/shibokengenerator.h | 33 +- sources/shiboken6/libshiboken/CMakeLists.txt | 4 +- sources/shiboken6/libshiboken/autodecref.h | 8 +- sources/shiboken6/libshiboken/basewrapper.cpp | 96 +++- sources/shiboken6/libshiboken/basewrapper.h | 11 +- .../shiboken6/libshiboken/bindingmanager.cpp | 24 +- .../libshiboken/embed/module_collector.py | 9 +- .../libshiboken/embed/signature_bootstrap.py | 24 +- sources/shiboken6/libshiboken/gilstate.cpp | 12 +- sources/shiboken6/libshiboken/gilstate.h | 3 +- sources/shiboken6/libshiboken/helper.cpp | 90 ++-- sources/shiboken6/libshiboken/helper.h | 24 +- sources/shiboken6/libshiboken/pep384impl.cpp | 25 - sources/shiboken6/libshiboken/pep384impl.h | 50 +- .../libshiboken/sbkarrayconverter.cpp | 1 - .../shiboken6/libshiboken/sbkbindingutils.cpp | 76 +++ .../shiboken6/libshiboken/sbkbindingutils.h | 40 ++ .../shiboken6/libshiboken/sbkconverter.cpp | 14 +- sources/shiboken6/libshiboken/sbkconverter.h | 10 +- sources/shiboken6/libshiboken/sbkenum.cpp | 50 +- sources/shiboken6/libshiboken/sbkenum.h | 16 + sources/shiboken6/libshiboken/sbkerrors.cpp | 10 + sources/shiboken6/libshiboken/sbkerrors.h | 2 + .../shiboken6/libshiboken/sbkfeature_base.cpp | 69 +-- sources/shiboken6/libshiboken/sbkmodule.cpp | 33 +- sources/shiboken6/libshiboken/sbkstring.cpp | 2 +- sources/shiboken6/libshiboken/shiboken.h | 1 + .../libshiboken/signature/signature.cpp | 28 +- .../signature/signature_helper.cpp | 2 +- sources/shiboken6/shiboken_tool.py | 2 +- .../shiboken6/shibokenmodule/_config.py.in | 2 +- .../files.dir/shibokensupport/feature.py | 7 +- .../shibokensupport/signature/layout.py | 222 ++++---- .../shibokensupport/signature/lib/enum_sig.py | 20 +- .../signature/lib/pyi_generator.py | 44 +- .../shibokensupport/signature/mapping.py | 29 +- .../shibokensupport/signature/parser.py | 49 +- .../shiboken6/tests/dumpcodemodel/main.cpp | 20 +- .../shiboken6/tests/libsample/CMakeLists.txt | 1 + .../tests/libsample/derivedusingct.cpp | 5 + .../tests/libsample/derivedusingct.h | 4 + sources/shiboken6/tests/libsample/moveonly.h | 39 ++ .../tests/libsample/samplenamespace.cpp | 1 + .../tests/otherbinding/typesystem_other.xml | 2 +- .../tests/qtxmltosphinx/CMakeLists.txt | 1 + .../shiboken6/tests/qtxmltosphinx/main.cpp | 10 +- .../tests/qtxmltosphinxtest/CMakeLists.txt | 1 + .../qtxmltosphinxtest/qtxmltosphinxtest.cpp | 19 +- .../qtxmltosphinxtest/qtxmltosphinxtest.h | 2 +- .../tests/samplebinding/CMakeLists.txt | 2 + .../tests/samplebinding/derived_test.py | 1 + .../shiboken6/tests/samplebinding/global.h | 1 + .../tests/samplebinding/objecttype_test.py | 14 +- .../samplebinding/objecttypelayout_test.py | 4 +- .../ownership_reparenting_test.py | 6 +- .../pointerprimitivetype_test.py | 3 +- .../tests/samplebinding/protected_test.py | 20 +- .../tests/samplebinding/sample_test.py | 7 + .../tests/samplebinding/samplesnippets.cpp | 250 +++++++++ .../tests/samplebinding/typesystem_sample.xml | 402 +++----------- .../tests/smartbinding/typesystem_smart.xml | 16 - .../tests/test_generator/dummygentest.cpp | 7 +- testing/command.py | 20 +- testing/parser.py | 37 +- testing/runner.py | 10 +- testing/wheel_tester.py | 6 +- .../android_utilities.py | 15 +- tools/cross_compile_android/main.py | 2 +- .../templates/toolchain_default.tmpl.cmake | 6 +- tools/doc_modules.py | 5 +- tools/example_gallery/main.py | 2 +- tools/snippets_translate/converter.py | 3 + tools/snippets_translate/enum_migration.py | 69 +++ .../snippets_translate.pyproject | 2 +- .../tests/test_converter.py | 4 + wheel_artifacts/pyproject.toml.base | 4 +- 727 files changed, 14549 insertions(+), 7158 deletions(-) delete mode 100644 .QT-ENTERPRISE-LICENSE-AGREEMENT create mode 100644 .gitreview create mode 100644 SECURITY.md create mode 100644 coin/fetch_libclang_arm64.ps1 create mode 100644 doc/changelogs/changes-6.8.3 create mode 100644 doc/changelogs/changes-6.9.0 create mode 100644 doc/changelogs/changes-6.9.1 create mode 100644 doc/changelogs/changes-6.9.2 create mode 100644 examples/demos/documentviewer/doc/imageviewer.py.rstinc create mode 100644 examples/demos/documentviewer/imageviewer/imageviewer.py create mode 100644 examples/graphs/2d/graphsaudio/GraphsAudio/Main.qml create mode 100644 examples/graphs/2d/graphsaudio/GraphsAudio/qmldir create mode 100644 examples/graphs/2d/graphsaudio/doc/graphsaudio.rst create mode 100644 examples/graphs/2d/graphsaudio/doc/graphsaudio.webp create mode 100644 examples/graphs/2d/graphsaudio/graphsaudio.pyproject create mode 100644 examples/graphs/2d/graphsaudio/main.py create mode 100644 examples/webenginewidgets/simplebrowser/ui_webauthdialog.py create mode 100644 examples/webenginewidgets/simplebrowser/webauthdialog.py create mode 100644 examples/webenginewidgets/simplebrowser/webauthdialog.ui delete mode 100644 examples/widgets/tutorials/addressbook/addressbook.pyproject delete mode 100644 examples/widgets/tutorials/addressbook/doc/addressbook.png delete mode 100644 examples/widgets/tutorials/addressbook/doc/addressbook.rst delete mode 100644 examples/widgets/tutorials/addressbook/part1.py delete mode 100644 examples/widgets/tutorials/addressbook/part2.py delete mode 100644 examples/widgets/tutorials/addressbook/part3.py delete mode 100644 examples/widgets/tutorials/addressbook/part4.py delete mode 100644 examples/widgets/tutorials/addressbook/part5.py delete mode 100644 examples/widgets/tutorials/addressbook/part6.py delete mode 100644 examples/widgets/tutorials/addressbook/part7.py create mode 100644 sources/pyside-tools/project_lib/pyproject_json.py create mode 100644 sources/pyside-tools/project_lib/pyproject_parse_result.py create mode 100644 sources/pyside-tools/project_lib/pyproject_toml.py create mode 100644 sources/pyside6/PySide6/glue/qtremoteobjects.cpp delete mode 100644 sources/pyside6/PySide6/templates/opengl_common.xml create mode 100644 sources/pyside6/doc/developer/fix_type_hints.rst create mode 100644 sources/pyside6/doc/developer/remoteobjects.md create mode 100644 sources/pyside6/doc/tutorials/portingguide/chapter3/chapter3.pyproject create mode 100644 sources/pyside6/doc/tutorials/portingguide/chapter3/rc_books.py create mode 100644 sources/pyside6/doc/tutorials/portingguide/chapter3/ui_bookwindow.py create mode 100644 sources/pyside6/libpysideremoteobjects/CMakeLists.txt create mode 100644 sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp create mode 100644 sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp create mode 100644 sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h create mode 100644 sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h create mode 100644 sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp create mode 100644 sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h create mode 100644 sources/pyside6/tests/QtCore/qmessage_logger_test.py create mode 100644 sources/pyside6/tests/QtGui/qpaintengine_test.py create mode 100644 sources/pyside6/tests/QtRemoteObjects/cpp_interop/CMakeLists.txt create mode 100644 sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop.cpp create mode 100644 sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop_test.py create mode 100644 sources/pyside6/tests/QtRemoteObjects/dynamic_types_test.py create mode 100644 sources/pyside6/tests/QtRemoteObjects/integration_test.py create mode 100644 sources/pyside6/tests/QtRemoteObjects/repfile_test.py create mode 100644 sources/pyside6/tests/QtRemoteObjects/simple.rep create mode 100644 sources/pyside6/tests/QtRemoteObjects/test_shared.py create mode 100644 sources/pyside6/tests/QtSerialBus/QtSerialBus.pyproject create mode 100644 sources/pyside6/tests/QtSerialBus/serialbus.py create mode 100644 sources/pyside6/tests/QtSerialBus/test.dbc create mode 100644 sources/pyside6/tests/QtWidgets/pyside3069.py rename sources/pyside6/tests/{QtGui => QtWidgets}/pyside_reload_test.py (55%) create mode 100644 sources/pyside6/tests/signals/nonqobject_receivers_test.py rename sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/{.pyproject => Drumpad.pyproject} (100%) create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/pyproject.toml create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/example_project.pyproject create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/folder/label_in_folder.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/main.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/mainwindow.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/pyproject.toml create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject create mode 100644 sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject_button.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/existing_pyproject_toml.pyproject create mode 100644 sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml create mode 100644 sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/main.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/pyproject.toml create mode 100644 sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/zzz.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/invalid_pyproject.pyproject create mode 100644 sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/main.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/pyproject.toml create mode 100644 sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/valid_pyproject.pyproject create mode 100644 sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/common_file.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/expected_pyproject.toml create mode 100644 sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file1.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file2.py create mode 100644 sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project1.pyproject create mode 100644 sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project2.pyproject delete mode 100644 sources/pyside6/tests/util/processtimer.py delete mode 100644 sources/pyside6/tests/util/test_processtimer.py create mode 100644 sources/shiboken6/ApiExtractor/cpptypeentry.h create mode 100644 sources/shiboken6/ApiExtractor/filecache.cpp create mode 100644 sources/shiboken6/ApiExtractor/filecache.h create mode 100644 sources/shiboken6/doc/typesystem_objectvalue.rst create mode 100644 sources/shiboken6/doc/typesystem_overloads.rst create mode 100644 sources/shiboken6/generator/shiboken/overridecacheentry.cpp create mode 100644 sources/shiboken6/generator/shiboken/overridecacheentry.h create mode 100644 sources/shiboken6/libshiboken/sbkbindingutils.cpp create mode 100644 sources/shiboken6/libshiboken/sbkbindingutils.h create mode 100644 sources/shiboken6/tests/libsample/moveonly.h create mode 100644 tools/snippets_translate/enum_migration.py diff --git a/.QT-ENTERPRISE-LICENSE-AGREEMENT b/.QT-ENTERPRISE-LICENSE-AGREEMENT deleted file mode 100644 index 2d225ecf..00000000 --- a/.QT-ENTERPRISE-LICENSE-AGREEMENT +++ /dev/null @@ -1,262 +0,0 @@ -Qt Frame Agreement -Version 2024-02 - -1. PARTIES OF THIS AGREEMENT -1.1. This Qt Frame Agreement—comprised of these general terms together with the appendices attached hereto, (hereinafter “Agreement”) is made by and between: The Qt Company, as defined below (hereinafter ”The Qt Company”) AND Customer name (hereinafter “Customer"):___________________ Business Id (e.g. VAT or EIN number):___________________ -1.2. The parties above are hereinafter individually referred to as a "Party" and collectively as the "Parties". - -2. STRUCTURE AND OBJECT OF THE AGREEMENT -2.1. The Parties have entered into this Agreement to agree on the terms and conditions applicable to The Qt Company's delivery of products and services ("Services") to Customer. -2.2. This Agreement is comprised of the following components: -(i) This Agreement, which contains the general terms applicable to all Services, -(ii) Appendices for each of the Services, containing terms applicable to that individual set of Services ("Service Terms"), -(iii) a Qt Appendix for Pricing, if applicable, which contains pricing for specific Services, and -(iv) other topic-specific appendices, such as Support or Marketing Rights. -2.3. Any and all Services purchased shall be specified in, and agreed upon between, the Parties under a separate purchase order, statement of work, quote, or similar document ("Purchase Document"). Each Purchase Document concluded under this Agreement shall include a reference to this Agreement and be governed by this Agreement. - -3. DEFINITIONS -3.1. "Affiliate" of a Party shall mean an entity (i) which is directly or indirectly controlling such Party; (ii) which is under the same direct or indirect ownership or control as such Party; or (iii) which is directly or indirectly owned or controlled by such Party. For these purposes, an entity shall be treated as being controlled by another if that other entity has fifty percent (50 %) or more of the votes in such entity, is able to direct its affairs and/or to control the composition of its board of directors or equivalent body. -3.2. "Contractor" shall mean third-party consultants, distributors and contractors performing services to Customer under an applicable contractual arrangement. -3.3. "Customer" shall mean the individual or legal entity specified in Section 1 above, that is a Party to this Agreement. -3.4. "Force Majeure Event" shall have the meaning set forth in Section 11.7. -3.5. "Licensed Software" shall mean The Qt Company's commercial software product which is licensed for use by Customer under this Agreement and corresponding Service Terms. Licensed Software shall include, if and to the extent applicable and specified in the applicable relevant Service Terms, corresponding online or electronic documentation, associated media and printed materials, including the source code, and example programs. The Qt Company may in the course of its development activities, at its free and absolute discretion and without any obligation to send or publish any notifications to Customer or in general, make changes, additions or deletions in the components and functionalities of the Licensed Software, provided that no such changes, additions or deletions will affect the already released version of the Licensed Software, but only upcoming version(s). Licensed Software is commercial computer software, developed at private expense and offered to the public under standard commercial terms. -3.6. "Professional Services" shall mean The Qt Company's professional-, consulting-, training- and/or project services delivered to Customer under this Agreement and specified in a Purchase Document. -3.7. "Support" shall mean maintenance and support services provided by The Qt Company to assist Customer in using the Licensed Software, as further specified in the Appendix for Support Terms. -3.8. "The Qt Company" shall mean: -(i) in the event Customer is an entity residing in the United States or a legal entity incorporated in or having its headquarters in the United States, The Qt Company Inc., a Delaware corporation with its office at 3031 Tisch Way, 110 Plaza West, San Jose, CA 95128, USA.; or -(ii) in the event Customer is an entity residing outside of the United States or a legal entity incorporated or having its registered office outside of the United States, The Qt Company Oy., a Finnish company with its registered office at Miestentie 7, 02150 Espoo, Finland. - -4. PRICES AND PAYMENT -4.1. The Qt Company agrees to make Services available to Customer subject to the prices set forth in the Appendix for Pricing. In the event that the Appendix for Pricing does not include a price for certain Services, the applicable price shall be the price agreed by the Parties in the respective Purchase Document. -4.2. All prices are exclusive of value added tax or other taxes, levels, or duties. Value added tax as well as other possible public charges imposed by authorities shall be added to the prices. -4.3. All fees under this Agreement are non-cancellable and non-refundable. -4.4. All fees under this Agreement shall be paid by Customer no later than thirty (30) days from the date of the applicable invoice from The Qt Company. -4.5. Unless otherwise agreed or provided in the respective Service Terms or Purchase Document, The Qt Company will invoice fees for: -4.5.1. Licensed Software and Support in advance upon conclusion of the Purchase Document, and -4.5.2. Professional Services monthly in arrears after the Service has been performed. -4.6. A late payment charge of the lower of: (a) one percent (1%) per month; or (b) the highest interest rate stipulated by applicable law, shall be charged on any unpaid balances that remain past due and which have not been disputed by Customer in good faith within thirty (30) days of receipt of invoice from The Qt Company. -4.7. The Qt Company may either (i) invoice Customer based on existing agreement, (ii) request Customer to place a purchase order corresponding to a quote by The Qt Company, or (iii) use Customer's stored Credit Card information to automatically charge the Customer for the relevant Renewal Term. -4.8. Unless and to the extent otherwise agreed in the Appendix for Pricing or in the Purchase Document, The Qt Company shall be entitled to adjust the prices set forth in the Appendix for Pricing by notifying Customer of the change in writing at least sixty (60) days before the effective date of the change. The change shall not affect the current pricing term of Services agreed upon before the effective date of the change. - -5. CONFIDENTIALITY -5.1. The Parties shall keep confidential, and shall not use or disclose to any unauthorized third parties, the existence and content of this Agreement as well as any Confidential Information received from the other Party or otherwise learned in connection with the Agreement or the performance of the Services, without the prior written consent of the other Party. Confidential Information shall mean information that is designated as confidential or that would be reasonably understood to be confidential given the circumstances of disclosure and the nature of the information. The Parties shall not use Confidential Information received from the other Party for any other purposes than the performance of the Agreement or the fulfilment of their rights and obligations hereunder. -5.2. Each Party shall limit access the other Party's Confidential Information only to those of its employees, subcontractors, Contractors, Affiliates or financial or legal advisors who necessarily need access to the Confidential Information for the proper performance of the Party's rights and obligations under the Agreement. Each Party shall ensure that the persons receiving Confidential Information of the other Party are bound by confidentiality obligations not less restrictive than those stipulated herein. -5.3. Each Party shall protect the confidentiality of the other Party's Confidential Information with at least the same degree of security as it exercises to its own confidential information, but no less than a standard of reasonable care. -5.4. The confidentiality obligation stipulated herein shall not be applied to material and information which: -(iii) has become generally available or otherwise public prior to its submission by the other Party; -(iv) becomes generally available or otherwise public due to a reason other than the negligence or omission of the recipient or its personnel or other actions in violation of this Agreement or applicable legislation; -(v) the Party has lawfully received from a third party without any obligation of confidentiality; -(vi) was lawfully in the possession of the receiving Party prior to receipt of the same from the other Party without any obligation of confidentiality related thereto; -(vii) a Party has developed independently without using material or information received from the other Party; or -(viii) a Party must disclose pursuant to law, decree or other order issued by competent regulatory or governmental body or other public authority or a judicial order, in which case the Party shall, to the extent permitted by applicable law, inform the other Party in writing of the disclosure of information prior to such disclosure. -5.5. Each Party shall, upon request of the other Party at any time, including upon termination, cancellation or expiry of the Agreement, promptly destroy or deliver to the other Party any and all the documents, files, copies and material containing Confidential Information of the other Party. Notwithstanding the foregoing, a Party may retain one copy of the Confidential Information in a secure location, if and solely to the extent required to comply with applicable laws or regulations. Any Confidential Information stored in electronic back-up form shall be rendered inaccessible and destroyed in accordance with standard back-up procedures. - -6. INTELLECTUAL PROPERTY RIGHTS -6.1. Unless and to the extent expressly provided in the respective Service Terms, this Agreement carries no assignment or license to the intellectual property rights of either Party and all such rights are and shall remain the exclusive property of the Party to whom such rights are vested under applicable law at the signing of this Agreement or thereafter. -6.2. Where The Qt Company's delivery includes any materials owned by a third party, such third party materials shall be governed in all respects by the applicable license terms of such third-party right holders. The Qt Company shall duly inform the Customer whenever such third party materials are included in the Services and of applicable license terms to be followed by the Customer in using such third party materials. - -7. FEES AND ORDERING -7.1. Services Fees. Services Fees are described in the Purchase Document. -7.2. Ordering Services. -(i) Customer may purchase Services pursuant to agreed pricing terms or, if no specific pricing terms have been agreed upon, at The Qt Company's standard pricing terms applicable at the time of purchase. -(ii) Unless expressly otherwise agreed, any price or other term quoted to Customer shall only be valid for the thirty (30) days from the date such price has been quoted. - -8. LIMITED WARRANTY AND WARRANTY DISCLAIMER -8.1. The Qt Company hereby represents and warrants that: (i) it has the power and authority to grant the rights and licenses granted to Customer under this Agreement; (ii) the Licensed Software will operate materially in accordance with its specifications (as set forth in the applicable product documentation or, where relevant, program description); (iii) Professional Services and Support will be performed in a professional, workmanlike manner pursuant to the Agreement; and (iv) during the ten years prior to the effective date of this Agreement, there have not been any claims alleging that the Licensed Software has infringed any intellectual property rights of a third party and, to the knowledge of The Qt Company as of the effective date of this Agreement, no such infringement exists. These warranties do not apply to issues arising from, or relating to, any third-party materials or Customer's use of the Licensed Software in violation of applicable law or the terms of this Agreement. -8.2. Except to the extent set forth above, the Services are delivered to Customer "as is" and to the maximum extent permitted by applicable law, exclusive of other warranties, whether express, implied, or otherwise. Customer's sole and exclusive remedy and The Qt Company's entire liability for deficiencies or errors in the Services shall be limited, at The Qt Company's option, to correction of the error, replacement of the Services, re-performance of the Service or return of the applicable fees paid for the defective Service for the time period during which Customer was not able to utilize the Service as agreed. - -9. LIMITATION OF LIABILITY -9.1. EXCEPT FOR (I) CASES OF GROSS NEGLIGENCE OR INTENTIONAL MISCONDUCT, (II) A BREACH OR VIOLATION OF THE OTHER PARTY'S INTELLECTUAL PROPERTY RIGHTS, OR (III) WHERE REQUIRED BY APPLICABLE LAW, IN NO EVENT SHALL EITHER PARTY BE LIABLE TO THE OTHER PARTY FOR ANY LOST PROFITS, LOSS OF DATA, LOSS OF BUSINESS OR GOODWILL OR ANY OTHER INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE COST, DAMAGES OR EXPENSE OF ANY KIND, HOWSOEVER ARISING UNDER OR IN CONNECTION WITH THIS AGREEMENT. -9.2. EXCEPT FOR (I) CASES OF GROSS NEGLIGENCE OR INTENTIONAL MISCONDUCT, (II) A BREACH OR VIOLATION OF THE OTHER PARTY'S INTELLECTUAL PROPERTY RIGHTS, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL EITHER PARTY'S TOTAL AGGREGATE LIABILITY UNDER THIS AGREEMENT EXCEED THE AGGREGATE FEES PAID OR PAYABLE TO THE QT COMPANY BY CUSTOMER FOR THE RESPECTIVE LICENSED SOFTWARE OR SERVICE GIVING RISE TO THE LIABILITY. THE FOREGOING LIMITATION WILL NOT APPLY TO CUSTOMER'S OBLIGATION TO PAY THE APPLICABLE FEES CORRESPONDING TO ITS ACTUAL USE OF LICENSED SOFTWARE OR SERVICES. - -10. TERM AND TERMINATION -10.1. This Agreement shall enter into force upon signing by both Parties and is effective as of the last date of signature. -10.2. This Agreement shall remain in force until further notice and may be terminated without cause by either Party by no less than three (3) months' prior written notice to the other Party. -10.3. Termination of a particular Purchase Document and the Services governed thereunder shall be stipulated under the applicable Service Terms. -10.4. Either Party may terminate this Agreement with immediate effect, if the other Party: -(i) commits a material breach of the terms of this Agreement (including applicable Service Terms) and has not remedied such breach within a reasonable period of time (which shall be no less than thirty (30) days) of the non-breaching Party's written notice specifying the breach, or -(ii) becomes bankrupt, insolvent or goes into liquidation or debt restructuring. -10.5. Termination of this Agreement shall, as such, have no effect on the validity of any Services ordered and agreed prior to the effective date of such termination, and such Services shall continue to remain in force pursuant to applicable Service Terms (including the terms of this Agreement) for the remainder of the duration of the applicable Service validity term. - -11. GOVERNING LAW AND DISPUTE RESOLUTION -11.1. The United Nations Convention on Contracts for the International Sale of Goods will not apply to this Agreement. -11.2. Where this Agreement is concluded with The Qt Company, Inc., a Delaware corporation, the Parties agree that this Section 10.2 will apply. This Agreement will be governed by, and construed in accordance with the laws of the State of California and any controlling United States federal law. Any dispute, controversy or claim arising out of or relating to this contract, including the formation, interpretation, breach or termination thereof, and whether the claims asserted are arbitrable, will be referred to and finally determined by arbitration in accordance with the JAMS International Arbitration Rules. The tribunal will consist of one arbitrator. The place of arbitration will be San Francisco, California, USA. The language to be used in the arbitral proceedings will be English. Judgment upon the award rendered by the arbitrator(s) may be entered in any court having jurisdiction thereof. This Section 10.2 shall not preclude parties from seeking provisional remedies in aid of arbitration from a court of appropriate jurisdiction. Notwithstanding the foregoing, any action by The Qt Company solely to collect license or other fees hereunder may be brought in any court of competent jurisdiction. -11.3. Where this Agreement is concluded with The Qt Company, Oy., a Finnish company, the parties agree that this Section 10.3 will apply. This Agreement shall be construed and interpreted in accordance with the laws of Finland, excluding its choice of law provisions. All disputes arising out of or in connection with this Agreement shall be finally settled in accordance with the laws of Finland, excluding its choice of law provisions. All disputes arising out of or in connection with this Agreement shall be finally settled under the Rules of Arbitration of the International Chamber of Commerce by one or more arbitrators appointed in accordance with the said Rules. The place of arbitration will be Helsinki, Finland. The language to be used in arbitral proceedings will be English. This Section 10.3 shall not preclude parties from seeking provisional remedies in aid of arbitration from a court of appropriate jurisdiction. - -12. MISCELLANOUS -12.1. No Assignment. Customer shall not be entitled to assign or transfer all or any of its rights, benefits and obligations under this Agreement except in case of sale of relevant business or assets or otherwise with prior written consent of The Qt Company, which shall not be unreasonably withheld or delayed. The Qt Company shall be entitled to freely assign or transfer any of its rights, benefits or obligations under this Agreement. -12.2. Surviving Sections. Any terms and conditions that by their nature or otherwise reasonably should survive termination of this Agreement shall so be deemed to survive. -12.3. Entire Agreement. This Agreement, its Appendices and any applicable Purchase Documents constitute the complete agreement between the Parties and supersedes all prior or contemporaneous discussions, representations, contracts (including prior License Agreements and similar prior agreements), and proposals, written or oral, with respect to the subject matters discussed herein. -12.4. Subcontractors. The Qt Company may utilize subcontractors in the performance of Services under this Agreement, provided that The Qt Company remains responsible for the performance of the Services and compliance with this Agreement, as well as ensuring that subcontractors are required to abide by relevant restrictions (e.g., confidentiality) set forth in this Agreement. -12.5. Modifications. No modification of this Agreement shall be effective unless contained in a writing executed by an authorized representative of each Party. No standard terms and conditions or provisions of any Customer purchase order or other ordering form that Customer may use in connection with the acquisition of Services will modify or affect this Agreement, the parties agree that any such terms and conditions are void with no legal effect. -12.6. Affiliate Orders. Customer Affiliates may purchase Services via this Agreement as follows: -(i) any purchases by Customer Affiliates from The Qt Company or its Affiliates will create a contractual relationship directly between the relevant The Qt Company entity and the respective ordering Customer Affiliate; -(ii) the entry into a Purchase Document between The Qt Company and Customer Affiliate creates an agreement between The Qt Company and Customer Affiliate and incorporates all terms and conditions of this Agreement as the governing agreement between The Qt Company and Customer Affiliate ("Accession Agreement"): and -(iii) Customer Affiliate will be deemed "Customer" under the terms of this Agreement and all rights and obligations under such Accession Agreement are vested and borne solely by the ordering Customer Affiliate and the relevant The Qt Company entity as contracting parties under such Accession Agreement. -12.7. Force Majeure. Neither Party shall be liable to the other for any delay or non-performance of its obligations hereunder in the event and to the extent that such delay or non-performance is due to an event of act of God, terrorist attack or other similar unforeseeable catastrophic event that prevents either Party for fulfilling its obligations under this Agreement and which such Party cannot avoid or circumvent ("Force Majeure Event"). If the Force Majeure Event results in a delay or non-performance of a Party for a period of three (3) months or longer, then either Party shall have the right to terminate the relevant Purchase Document and Services thereunder with immediate effect without any liability (except for the obligations of payment arising prior to the Force Majeure Event) towards the other Party. -12.8. Notices. Any notice given by one Party to the other shall be deemed properly given and deemed received if specifically acknowledged by the receiving Party in writing or when successfully delivered to the recipient by hand, fax, or special courier during normal business hours on a business day to the addresses specified for each Party in this Agreement. Each communication and document made or delivered by one Party to the other Party pursuant to this Agreement shall be in the English language. -12.9. Attorney Fees. The prevailing Party in any action to enforce this Agreement shall be entitled to recover its attorney's fees and costs in connection with such action. -12.10. Privacy and Security. The Parties commit to and comply with their respective applicable obligations under the privacy and security terms set forth in the Privacy and Security Appendix and relevant Appendices attached hereto. -12.11. Feedback. Customer agrees that, from time to time, The Qt Company, may request feedback from Customer regarding the Services ("Feedback"). Customer may choose to provide Feedback and agrees that The Qt Company may freely use, copy, disclose, and exploit any Feedback. No Feedback will be considered Customer Confidential Information unless explicitly agreed otherwise between the Parties. -12.12. Export Control. Customer acknowledges that the Services, or portions thereof, may be subject to export control restrictions under the applicable laws of respective countries. Customer shall fully comply with all applicable export license restrictions and requirements, economic sanctions restrictions, as well as with all laws and regulations relating thereto, and shall procure all necessary governmental authorizations, including without limitation, all necessary licenses, approvals, permissions, or consents, where necessary (e.g., for re-exportation of the Redistributables, Applications and/or Devices, each as defined in the relevant Service Terms). -12.13. Severability. If any provision of this Agreement shall be adjudged by any court of competent jurisdiction to be unenforceable or invalid, that provision shall be limited or eliminated to the minimum extent necessary so that this Agreement shall otherwise remain in full force and effect and enforceable. - -13. APPENDICES -13.1. The following appendices form an integral part of this Agreement. In case of a discrepancy between this Agreement and any of its Appendices, this Agreement shall prevail. In case of discrepancies between the Purchase Document(s) and this Agreement or applicable Service Terms, the terms of this Agreement or the applicable Service Terms shall prevail, except in cases where an express deliberate deviation from the terms of this Agreement or applicable Service Terms has been concluded pursuant to Section 2.3 hereof, in which case the Purchase Document shall prevail. -1. Appendix for Qt Development Framework -2. Appendix for Support Terms https://www.qt.io/terms-conditions/support-terms -3. Appendix for Privacy and Security Terms https://www.qt.io/terms-conditions/privacy-and-security - -Appendix for Qt Development Framework -Version 2024-02 - -1. This Appendix for Qt Development Framework is an integral part of the Agreement and specifies the legal terms for the licensing of Licensed Software (as defined below) between The Qt Company and the Customer. Entry into this Appendix governs the use of and supersedes any prior contracts between the Parties (including prior License Agreements and similar prior agreements), with respect to the Licensed Software under this Appendix. - -2. DEFINITIONS -2.1. Capitalized words used in this Appendix shall have the meanings described in the Agreement or as defined below. -2.2. "Add-on Products" shall mean The Qt Company's specific add-on software products which are not licensed as part of The Qt Company's standard Services offerings, but shall be included into the scope of Licensed Software only if so specifically agreed between the Parties. -2.3. "Application" means software products created using the Licensed Software, which include the Redistributables, or part thereof. -2.4. "End Customer" shall mean Customer's customer(s) to whom Customer, directly or indirectly, distributes copies of the Redistributables as integrated or incorporated into Applications or Devices. -2.5. "Data Protection Legislation" shall mean the General Data Protection Regulation (EU 2016/679) (GDPR) and any national implementing laws, regulations and secondary legislation, as may be amended or updated from time to time, as well as any other data protection laws or regulations applicable in the relevant territory. -2.6. "Deployment Platforms" shall mean target operating systems and/or hardware specified in the License Certificate, on which the Redistributables can be distributed pursuant to the terms and conditions of this Appendix. -2.7. "Designated User(s)" shall mean the employee(s) of Customer or Customer's Affiliates acting within the scope of their employment or Customer's Contractors acting within the scope of their services on behalf of Customer. -2.8. "Development License" shall mean the license needed by the Customer for each Designated User to use Licensed Software under the license grant described in Section 5 of this Appendix. Development Licenses are available per respective Licensed Software products; each product having its designated scope and purpose of use. -2.9. "Development Platforms" shall mean the host operating system(s) specified in the License Certificate, on which Licensed Software can be used under the Development License. -2.10. "Devices" shall mean -(i) hardware devices or products that -a. are manufactured and/or distributed by the Customer, its Affiliates, Contractors or End Customer, and -b. incorporate, integrate or link to Applications such that substantial functionality of such unit, when used by an End User, is provided by Application(s) or otherwise depends on the Licensed Software; or -(ii) Applications designed for the hardware devices specified in item (i). -Devices covered by this Appendix shall be specified in the Pricing Appendix or Purchase Document. -2.11. "Distribution License(s)" shall mean a royalty-bearing license required for any kind of sale, trade, exchange, loan, lease, rental or other distribution by or on behalf of Customer to a third party of Redistributables in connection with Devices pursuant to license grant described in Section 5.3 of this Appendix. Distribution Licenses are sold separately for each type of Device respectively and cannot be used for any other type of Devices. -2.12. "Distribution License Packs" shall mean set of prepaid Distribution Licenses for distribution of Redistributables, as defined in The Qt Company's standard price list, quote, Pricing Appendix or in the Purchase Document, as applicable. -2.13. "Evaluation License Term" shall mean a time period specified in the License Certificate for the Customer to use the relevant Licensed Software for evaluation purposes according to Section 5.6 of this Appendix. -2.14. "Intellectual Property Rights" shall mean patents (including utility models), design patents, and designs (whether or not capable of registration), chip topography rights and other like protection, copyrights, trademarks, service marks, trade names, logos or other words or symbols and any other form of statutory protection of any kind and applications for any of the foregoing as well as any trade secrets. -2.15. "License Certificate" shall mean a certificate generated by The Qt Company for each Designated User respectively upon their download of the Licensed Software, which will be available under the respective Designated User's Qt Account at account.qt.io. License Certificates will specify relevant information pertaining to the Licensed Software purchased by Customer and the license to the Licensed Software. -2.16. "License Fee" shall mean the fee charged to Customer for rights granted under this Appendix. -2.17. "Licensed Software" shall mean the specified product(s) of Qt Software which Customer has purchased and which is provided to Customer under the terms of this Appendix (including its Exhibits). Licensed Software shall include corresponding online or electronic documentation, associated media and printed materials, including source code (where applicable), example programs and the documentation. Licensed Software does not include Third Party Software (as defined in Section 6) or Qt Community Edition. The Qt Company may, in the course of its development activities, at its free and absolute discretion and without any obligation to send or publish any notifications to Customer or in general, make changes, additions or deletions in the components and functionalities of the Licensed Software, provided that no such changes, additions or deletions will affect the already released version of the Licensed Software, but only upcoming version(s). -2.18. "License Term" shall mean the agreed validity period of the Development License during which the relevant Licensed Software product can be used pursuant to this Appendix. The agreed License Term, as ordered and paid for by Customer, shall be memorialized in the applicable License Certificate. -2.19. "Customer's Records" shall mean books and records that contain information bearing on Customer's compliance with the Agreement, Customer's use of Qt Community Edition and/or the payments due to The Qt Company under the Agreement, including, but not limited to user information, assembly logs, sales records and distribution records. -2.20. "Modified Software" shall have the meaning as set forth below in Section 4. -2.21. "Qt Software" shall mean the development and design software of The Qt Company, which The Qt Company makes available under commercial and/or open source licenses as either the "Licensed Software" or the "Qt Community Edition". -2.22. "Permitted Software" shall mean third party products that are generally available to the public, which may include parts of Qt Community Edition or be developed using Qt Community Edition. -2.23. "Pre-Release Code" shall have the meaning as set forth in Section 7. -2.24. "Prohibited Combination" shall mean any effort to use, combine, incorporate, link or integrate Licensed Software with any software created with or incorporating Qt Community Edition, or use Licensed Software for creation of any such software. -2.25. "Qt Community Edition" shall mean the open source version of Qt Software available under the terms of the GNU Lesser General Public License, version 2.1 or later ("LGPL") or the GNU General Public License, version 2.0 or later ("GPL"). For clarity, Qt Community Edition shall not be provided, governed or used under this Appendix. -2.26. "Redistributables" shall mean the portions of Licensed Software as set forth in Exhibit 1 hereto that may be distributed pursuant to this Appendix in object code form only, including any relevant documentation. Where relevant, any reference to Licensed Software in this Appendix includes and refers to Redistributables. -2.27. "Renewal Term" shall mean an extension of the previous License Term as agreed between the Parties. -2.28. "Submitted Modified Software" shall have the meaning as set forth in Section 4.2 of this Appendix. -2.29. "Third-Party Software" shall have the meaning set forth in Section 6 of this Appendix. -2.30. "Updates" shall mean a release or version of the Licensed Software containing bug fixes, error corrections and other changes that are generally made available to users of the Licensed Software that have contracted for Support. Updates are generally depicted as a change to the digits following the decimal in the Licensed Software version number. The Qt Company shall make Updates available to Customer under the Support. Updates shall be considered as part of the Licensed Software hereunder. -2.31. "Upgrades" shall mean a release or version of the Licensed Software containing enhancements and new features and are generally depicted as a change to the first digit of the Licensed Software version number. In the event that Upgrades are provided to Customer under this Appendix, they shall be considered as part of the Licensed Software hereunder. - -3. OWNERSHIP -3.1. Ownership of The Qt Company -3.1.1. The Licensed Software is protected by copyright laws and international copyright treaties, as well as other intellectual property laws and treaties. The Licensed Software is licensed, not sold. -3.1.2. All of The Qt Company's Intellectual Property Rights are and shall remain the exclusive property of The Qt Company or its respective licensors . No rights to The Qt Company's Intellectual Property Rights are assigned or granted to Customer under this Appendix, except when and to the extent expressly specified herein. -3.2. Ownership of Customer -3.2.1. All of Customer's Intellectual Property Rights are and shall remain the exclusive property of Customer or its licensors respectively. -3.2.2. Except to the extent set forth in this Appendix, all Intellectual Property Rights to the Modified Software, Applications and Devices (except to Redistributables included therein) shall remain with Customer. - -4. MODIFIED SOFTWARE -4.1. Customer may create bug-fixes, error corrections, patches or modifications to the Licensed Software ("Modified Software"). To the extent that Customer's Modified Software breaks source or binary compatibility or other functionality with the Licensed Software, Customer acknowledges that The Qt Company's ability to provide Support may be prevented or limited and Customer's ability to make use of Updates may be restricted. -4.2. Customer may, at its sole and absolute discretion, choose to submit Modified Software to The Qt Company ("Submitted Modified Software") in connection with Customer's Support request, service request or otherwise. In the event Customer does so, then, Customer hereby grants The Qt Company a sublicensable, assignable, irrevocable, perpetual, worldwide, non-exclusive, royalty-free and fully paid-up license, under all of Customer's Intellectual Property Rights, to reproduce, adapt, translate, modify, and prepare derivative works of, publicly display, publicly perform, sublicense, make available and distribute such Submitted Modified Software as The Qt Company sees fit at its free and absolute discretion. - -5. LICENSES GRANTED -5.1. Development with Licensed Software -5.1.1. Subject to the terms of the Agreement, The Qt Company grants to Customer a worldwide, non-exclusive, non-transferable license, valid for each License Term, to use, modify and copy the Licensed Software by Designated Users on the Development Platforms for the sole purposes of designing, developing, demonstrating and testing Application(s) and/or Devices, and to provide support and other services related to such Applications and Devices to End Customers. Each Application and/or Device can only include, incorporate or integrate contributions by such Designated Users who are duly licensed for the applicable Development Platform(s) and Deployment Platform(s) (i.e have a valid license for the appropriate Licensed Software product and only use one type of Qt Development License per Customer Application and/or Device(s)). -5.1.2. Customer may install copies of the Licensed Software on five (5) computers per Designated User, provided that only Designated Users who have a valid Development License may use the Licensed Software. -5.1.3. Customer may designate another Designated User to replace a then-current Designated User by notifying The Qt Company in writing, where such replacement is due to termination of employment, long-term absence or other permanent reason affecting Designated User's need for Licensed Software. -5.1.4. Upon expiry of the initially agreed License Term, the respective License Term shall be automatically extended by one or more Renewal Term(s), unless and until either Party notifies the other Party in writing, that it does not wish to continue the License Term, such notification to be provided to the other Party no less than thirty (30) days before expiry of the respective License Term. The Qt Company shall, in good time before the due date for the above notification, remind the Customer on the coming Renewal Term. Unless otherwise agreed between the Parties, Renewal Term shall be equal to the length of the previous License Term, but no longer than thirty-six (36) months. -5.1.5. Any such Renewal Term shall be subject to License Fees agreed between the Parties or, if no advance agreement exists, subject to The Qt Company's standard list pricing applicable at the commencement date of any such Renewal Term. -5.2. Distribution of Applications -5.2.1. Subject to the terms of the Agreement, The Qt Company grants to Customer a worldwide, non-exclusive, non-transferable, perpetual, royalty-free and revocable (only for Customer’s material breach of agreement) right and license to: -(i) distribute, by itself or through its Contractors, Redistributables as installed, incorporated or integrated into Applications for execution on the Deployment Platforms; and -(ii) grant perpetual and irrevocable sublicenses to Redistributables, as distributed hereunder, for End Customers solely to the extent necessary in order for the End Customers to use the Applications for their respective intended purposes. -5.2.2. Right to distribute the Redistributables as part of an Application as provided herein is not royalty-bearing but is conditional upon the Application having been created, updated and maintained under a valid and duly paid Development License. -5.3. Distribution of Devices -5.3.1. Subject to the terms of the Agreement, The Qt Company grants to Customer a worldwide, non-exclusive, non-transferable, perpetual, revocable (only for Customer’s material breach of agreement), royalty-bearing right and license to: -(i) distribute, by itself or through one or more tiers of Contractors, Redistributables as installed, incorporated or integrated, or intended to be installed, incorporated or integrated into Devices for execution on the Deployment Platforms; and -(ii) grant perpetual and irrevocable sublicenses to Redistributables, as distributed hereunder, for End Customers solely to the extent necessary in order for the End Customers to use the Devices for their respective intended purposes. -5.3.2. Right to distribute the Devices as provided herein is conditional upon (i) the Devices having been created, updated and maintained under a valid and duly paid Development License, and (ii) Customer having acquired corresponding Distribution Licenses at the time of distribution of any Devices to End Customers. -5.4. Further Requirements -5.4.1. The licenses granted in this Section 5 by The Qt Company to Customer are conditional and subject to Customer's compliance with the following terms: -(i) Customer acknowledges that The Qt Company has separate products for the purpose of Applications and Devices respectively, where development and distribution of Devices is only allowed using the correct designated product. Customer shall ensure and bear the burden of proof that Customer is using a correct product entitling Customer to development and distribution of Devices; -(ii) Customer shall not remove or alter any copyright, trademark or other proprietary rights notice(s) contained in any portion of the Licensed Software; -(iii) Applications must add primary and substantial functionality to Licensed Software so as not to compete with the Licensed Software; -(iv) Applications may not pass on functionality which in any way makes it possible for others to create software with Licensed Software; provided however that Customer may use Licensed Software’s scripting and QML ("Qt Quick") functionality solely in order to enable scripting, themes and styles that augment the functionality and appearance of the Application(s) without adding primary and substantial functionality to the Application(s); -(v) Customer shall not use Licensed Software in any manner or for any purpose that infringes, misappropriates or otherwise violates any Intellectual Property Right or right of any third party, or that violates any applicable law; -(vi) Customer shall not use The Qt Company's or any of its suppliers' names, logos, or trademarks to market Applications, except that Customer may use “Built with Qt” logo to indicate that an Application or Device was developed using Licensed Software; -(vii) Customer shall not distribute, sublicense or disclose source code of Licensed Software to any third party (provided however that Customer may appoint employee(s) of Contractors and Affiliates as Designated Users to use Licensed Software pursuant to this Appendix). -(viii) Customer shall not grant the End Customers a right to: (a) make copies of the Redistributables except when and to the extent required to use the Applications and/or Devices for their intended purpose; (b) modify the Redistributables or create derivative works thereof; (c) decompile, disassemble or otherwise reverse engineer Redistributables; or (d) redistribute any copy or portion of the Redistributables to any third party, except as part of the onward sale of the Application or Device on which the Redistributables are installed; -(ix) Customer shall not, and shall cause that its Affiliates or Contractors shall not, use Licensed Software in any Prohibited Combination, unless Customer has received specific advance written permission from The Qt Company to do so. Absent such written permission, any and all distribution by Customer during the term of the Agreement of a hardware device or product: a) which incorporates or integrates any part of Licensed Software or Qt Community Edition; or b) where substantial functionality is provided by software built with Licensed Software or Qt Community Edition or otherwise depends on Licensed Software or Qt Community Edition, shall be considered to be Device distribution under this Appendix and shall be dependent on Customer’s compliance thereof (including but not limited to the obligation to pay applicable License Fees for such distribution). Notwithstanding the foregoing, Customer is entitled to use and combine Licensed Software with Permitted Software; -(x) Customer shall cause all of its Affiliates, Contractors and End Customer entitled to make use of the licenses granted under this Appendix, to be contractually bound to comply with the relevant terms hereof and not to use the Licensed Software beyond the terms hereof nor for any purposes other than operating within the scope of their services for Customer. Customer shall be responsible for any and all actions and omissions of its Affiliates, Contractors, and End Customers relating to the Licensed Software and use thereof (including but not limited to payment of all applicable License Fees); -(xi) Except when and to the extent explicitly provided in this Section 5, Customer shall not transfer, publish, disclose, display or otherwise make available the Licensed Software; and -(xii) Customer shall not attempt or enlist a third party to conduct or attempt to conduct any of the above. -5.4.2. The above terms shall not be applicable if and solely to the extent they conflict with any mandatory provisions of applicable laws. -5.4.3. Any use of Licensed Software beyond the provisions of this Appendix is strictly prohibited and requires, at a minimum an additional license from The Qt Company (e.g. certain additional rights granted under software development kit “SDK” agreement with regard to limitations of Section 5.4.1 iv, vii or viii). -5.5. Evaluation License -5.5.1. Subject to the terms of this Appendix, The Qt Company grants to Customer a worldwide, non-exclusive, non-transferable license, valid for the Evaluation License Term to use the relevant Licensed Software product solely for Customer’s internal use to evaluate and determine whether the Licensed Software meets Customer's business requirements, specifically excluding any commercial use of the Licensed Software or any derived work thereof. -5.5.2. Upon the expiry of the Evaluation License Term, Customer must either discontinue use of the relevant Licensed Software or acquire a commercial Development License specified herein. - -6. THIRD-PARTY SOFTWARE. The Licensed Software may provide links or access to third party libraries or code (collectively "Third-Party Software") to implement various functions. Third-Party Software does not, however, comprise part of the Licensed Software, but is provided to Customer complimentary and use thereof is discretionary for Customer. Third-Party Software will be listed in the ".../src/3rdparty" source tree delivered with the Licensed Software or documented in the Licensed Software, as such may be amended from time to time. Customer acknowledges that use or distribution of Third-Party Software is in all respects subject to applicable license terms of applicable third-party right holders. - -7. PRE-RELEASE CODE -7.1. The Licensed Software may contain pre-release code and functionality, or sample code marked or otherwise stated with appropriate designation such as "Technology Preview", "Alpha", "Beta", "Experimental", "Sample", "Example" etc. ("Pre-Release Code"). -7.2. Such Pre-Release Code may be provided complimentary for Customer, in order to provide experimental support or information for new platforms or preliminary versions of one or more new functionalities, or for other similar reasons. Pre-Release Code may not be at the level of performance and compatibility of a final, generally available, product offering. Pre-Release Code may not operate correctly, may contain errors and may be substantially modified by The Qt Company prior to a commercial product release, if any. The Qt Company is under no obligation to make Pre-Release Code commercially available, or provide any Support or Updates relating thereto. To the maximum extent permitted by law, the Qt Company assumes no liability whatsoever regarding any Pre-Release Code and any use thereof is exclusively at Customer's own risk and expense. -7.3. Unless Licensed Software specifies different license terms for the respective Pre-Release Code, Customer is entitled to use such pre-release code pursuant to Section 5 of this Appendix, just like other Licensed Software. - -8. SUPPORT. Support is provided according to agreed support level and subject to applicable requirements and restrictions, as specified in the Appendix for Support Terms. - -9. FEES AND ORDERING: DISTRIBUTION LICENSES -9.1. Distribution License Packs -9.1.1. Unless otherwise agreed in writing, Distribution Licenses shall be purchased by way of Distribution License Packs. -9.1.2. Upon due payment of the ordered Distribution License Pack(s), Customer will have an account of Distribution Licenses available for distributing the Redistributables in accordance with this Agreement. -9.2. Each time Customer distributes a copy of Redistributables, one Distribution License is used and Customer's account of available Distribution Licenses is decreased accordingly. -9.3. Customer may distribute copies of the Redistributables so long as Customer has Distribution Licenses remaining on its account. - -10. RECORD-KEEPING AND REPORTING OBLIGATIONS; AUDIT RIGHTS -10.1. Customer's Record-keeping -10.1.1. Customer shall at all times during the term of the Agreement or validity of any of the licenses hereunder, whichever is later, and for a period of two (2) years thereafter, maintain Customer's Records in an accurate and up-to-date form. Customer's Records shall be adequate to reasonably enable The Qt Company to determine Customer's compliance with the provisions of the Agreement. The records shall conform to general good accounting practices. -10.1.2. Customer shall, within thirty (30) days from receiving The Qt Company's request to that effect, deliver to The Qt Company a report based on Customer's Records, such report to contain information, in sufficient detail, on: (i) number and identity of users working with Licensed Software or Qt Community Edition, (ii) copies of Redistributables distributed by Customer during the most recent calendar quarter and/or any other term specified by The Qt Company, and (iii) any other information pertaining to Customer's compliance with the terms of the Agreement (e.g. information on products and/or projects relating to use of Distribution Licenses), as The Qt Company may reasonably require from time to time. -10.2. The Qt Company's Audit Rights -10.2.1. The Qt Company or an independent auditor acting on behalf of The Qt Company may, upon at least thirty (30) days' prior written notice and at The Qt Company expense, audit Customer with respect to Customer's use of the Licensed Software, but not more frequently than once during each six (6) month period. Such audit may be conducted by mail, electronic means or through an in-person visit to Customer's place of business. Any possible in-person audit shall be conducted during regular business hours at Customer's facilities, shall not unreasonably interfere with Customer's business activities and shall be limited in scope to verify Customer's compliance with the terms of the Agreement. The Qt Company or its independent auditor shall be entitled to inspect Customer's Records and conduct necessary interviews of Customer's relevant employees and Contractors. All Customer's Records and use thereof shall be subject to the obligation of confidentiality under the Agreement. -10.2.2. If an audit reveals that Customer is using the Licensed Software beyond scope of the licenses Customer has paid for, Customer shall pay to The Qt Company any amounts owed for such unauthorized use within thirty (30) days from receipt of the corresponding invoice from The Qt Company. -10.2.3. In addition, in the event the audit reveals a material violation of the terms of the Agreement (without limitation, either (i) underpayment of more than 10% of License Fees or 10,000 euros (whichever is more) or (ii) distribution of products, which include or result from Prohibited Combination, shall be deemed a material violation for purposes of this section), then Customer shall pay The Qt Company's reasonable cost of conducting such audit. - -11. TERMINATION -11.1. Termination of Licenses -11.1.1. The Qt Company may terminate Customer's rights to any and all Licensed Software (including access to Support), if Customer: -(i) commits a material breach of the Agreement (including this Appendix) and has not remedied the breach within a reasonable period of time (which shall be no less than 30 days) of The Qt Company's written notice specifying the breach, or -(ii) becomes bankrupt, insolvent or goes into liquidation or debt restructuring. -11.2. Suspension of rights: Instead of termination, The Qt Company reserves the right to suspend or withhold grants of any and all rights to the Licensed Software (including Support), should Customer fail to make payment in timely fashion or otherwise violate or is reasonably suspected of violating its obligations under the Agreement and/or this Appendix, and where such violation or breach is not cured within ten (10) business days following The Qt Company's written notice thereof. -11.3. Parties Rights and Duties upon Termination -11.3.1. Upon expiry or termination of the Development Licenses, Customer shall cease and shall cause all Designated Users (including those of its Affiliates' and Contractors') to cease using the relevant Licensed Software. -11.3.2. Upon such expiry or termination of Development Licenses, Customer shall destroy or return to The Qt Company all copies of the respective Licensed Software and all related materials and will certify the same by Customer's duly authorized officer to The Qt Company upon its request, provided however that Customer may retain and utilize such copies of the Licensed Software to the extent required to provide Customer's continued support to End Customers, for archiving purposes or as is required under applicable law. -11.3.3. Distribution Licenses are perpetual and, therefore, Customer's distribution rights hereunder shall only terminate upon The Qt Company's termination of Distribution Licenses due to Customer's material breach as set forth in Section 11.1.1(i) of this Appendix. In case of such termination by The Qt Company due to Customer's material breach, Customer must cease any distribution of Applications and Devices at the effective date of termination. -11.3.4. Expiry or termination of any of Customer's licenses hereunder for any reason whatsoever shall not: -(i) relieve Customer of its obligation to pay any License Fees accrued or payable to The Qt Company prior to the effective date of termination, and Customer pay to The Qt Company all such fees within 30 days from the effective date of termination of the licenses; -(ii) relieve Customer of its obligation to ensure that Applications and Devices (including those already distributed) remain in compliance with the terms of the Agreement; nor -(iii) affect any rights of End Customer to continue use of Applications and Devices (and therein incorporated Redistributables). -11.4. Extension of Rights under Special Circumstances. In the event that, during the applicable License Term, The Qt Company is declared bankrupt under a final, non-cancellable decision by relevant court of law, and the Agreement is not, at the date of expiry of the Development License(s), assigned to a party who has assumed The Qt Company's position as a legitimate licensor of Licensed Software under the Agreement, then all valid Development Licenses possessed by Customer at such date of expiry, and which Customer has not notified for expiry, shall be extended to be valid in perpetuity under the terms of the Agreement. Any such extension shall not apply to The Qt Company's Support obligations. - -EXHIBIT 1, Licensed Software -At the time of conclusion of this Appendix, the latest available version of Licensed Software includes the software libraries and tools set forth in Exhibit 1 (as provided below), depending on which product(s) Customer has purchased under the relevant Purchase Document. -The modules and tools are specific to each product version respectively and may vary from version to version. Modules and tools included in the latest publicly available version of the respective product at any given time are listed in Exhibit 1 of https://www.qt.io/terms-conditions/qt-dev-framework/exhibit-1. If a new version of Licensed Software does not include a module or tool present in an older version which Customer is entitled to use under a valid license from The Qt Company, then Customer will continue to have such right during the validity of Customer's license to relevant Licensed Software. In the event a new version of the Licensed Software adds modules or tools to any previous version(s), Customer's rights will extend to cover also such additional modules and tools. - -EXHIBIT 2 - Small Business Terms -1. This Exhibit applies to entities that qualify as a Qualified Small Business (defined below) and provides additional terms and conditions applicable to small business pricing and licensing. In the event that Customer is a Qualified Small Business and there is any conflict between the terms of this Exhibit and any other terms of the Agreement, the terms in this Exhibit shall take precedence. - -2. APPLICABILITY FOR SMALL BUSINESS LICENSES. Any small business discounts applied require that Customer (including any Customer Affiliates or group entities) has an annual revenue (including annual capital funding) below 1 Million EUR, or the equivalent thereof, as approved by The Qt Company (each, a "Qualified Small Business"). The annual revenue, including funding, must be evidenced upon request by business records and approved by The Qt Company in its reasonable discretion. - -3. SUPPORT. Support is limited to: (i) Install Support; and (ii) for any other Standard Support issue, five (5) support tickets annually. - -4. LIMITATION ON NUMBER OF SMALL BUSINESS DEVELOPER LICENSES. Qualified Small Business discounts and purchasing structure may be applied to a maximum of three discounted developer licenses (either ADE or DCP) per Qualified Small Business. Any additional licenses purchased will be at The Qt Company list price in effect at the time. - -5. LIMITATION FOR NUMBER OF INSTALLATIONS. Customer may install copies of the Licensed Software on two (2) computers per Designated User, provided that only the Designated Users who have a valid Development License may use the Licensed Software. - -6. CONDITIONAL WAIVER OF DISTRIBUTION LICENSES. For Qualified Small Businesses, the Agreement requirements to purchase Distribution Licenses for Devices shall apply only when Customer ceases to be a Qualified Small Business (e.g., when annual revenue threshholds are bypassed). - -7. ADDITIONAL TERMS FOR RENEWALS. The initial subscription purchase term for Qualified Small Business Licenses is twelve (12) months. Upon expiration of the initial twelve (12) month term and unless terminated in accordance with the Agreement, the Licenses will automatically renew for additional twelve (12) month terms with applicable Qualified Small Business discounts. If Customer ceases to be a Qualified Small Business, renewal pricing shall be at The Qt Company list price in effect at the time of renewal, or as agreed in writing between the parties. - -8. ADDITIONAL AUDIT RIGHTS. In addition to the audit rights set forth in the Agreement, The Qt Company reserves the right to audit Customer financial records in order to determine whether Customer is a Qualified Small Business. diff --git a/.gitreview b/.gitreview new file mode 100644 index 00000000..e92d8e57 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=codereview.qt-project.org +project=pyside/pyside-setup +defaultbranch=dev diff --git a/README.pyside6.md b/README.pyside6.md index 9178660d..95e75a4c 100644 --- a/README.pyside6.md +++ b/README.pyside6.md @@ -88,7 +88,7 @@ and [join our community](https://wiki.qt.io/Qt_for_Python#Community)! ### Licensing -PySide6 is available under both Open Source (LGPLv3/GPLv3) and commercial +PySide6 is available under both Open Source (LGPLv3 or GPLv2 or GPLv3) and commercial license. Using PyPi is the recommended installation source, because the content of the wheels is valid for both cases. For more information, refer to the [Qt Licensing page](https://www.qt.io/licensing/). diff --git a/README.pyside6_addons.md b/README.pyside6_addons.md index a4257670..e232565a 100644 --- a/README.pyside6_addons.md +++ b/README.pyside6_addons.md @@ -67,7 +67,7 @@ and [join our community](https://wiki.qt.io/Qt_for_Python#Community)! ### Licensing -PySide6 is available under both Open Source (LGPLv3/GPLv3) and commercial +PySide6 is available under both Open Source (LGPLv3 or GPLv2 or GPLv3) and commercial license. Using PyPi is the recommended installation source, because the content of the wheels is valid for both cases. For more information, refer to the [Qt Licensing page](https://www.qt.io/licensing/). diff --git a/README.pyside6_essentials.md b/README.pyside6_essentials.md index 7f96c19b..73d8b66c 100644 --- a/README.pyside6_essentials.md +++ b/README.pyside6_essentials.md @@ -51,7 +51,7 @@ and [join our community](https://wiki.qt.io/Qt_for_Python#Community)! ### Licensing -PySide6 is available under both Open Source (LGPLv3/GPLv3) and commercial +PySide6 is available under both Open Source (LGPLv3 or GPLv2 or GPLv3) and commercial license. Using PyPi is the recommended installation source, because the content of the wheels is valid for both cases. For more information, refer to the [Qt Licensing page](https://www.qt.io/licensing/). diff --git a/README.pyside6_examples.md b/README.pyside6_examples.md index 15e31815..b176139d 100644 --- a/README.pyside6_examples.md +++ b/README.pyside6_examples.md @@ -28,7 +28,7 @@ and [join our community](https://wiki.qt.io/Qt_for_Python#Community)! ### Licensing -PySide6 is available under both Open Source (LGPLv3/GPLv3) and commercial +PySide6 is available under both Open Source (LGPLv3 or GPLv2 or GPLv3) and commercial licenses. Using PyPi is the recommended installation source, because the content of the wheels is valid for both cases. For more information, refer to the [Qt Licensing page](https://www.qt.io/licensing/). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..2b813565 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +This repository contains the source code for the PySide +, Shiboken, and Shiboken Generator packages, which are +under the Qt Project. + +The Qt Project has the security policy defined +in the [QUIP-15](https://contribute.qt-project.org/quips/15) + +For reporting a vulnerability, please follow the instructions +on [QUIP-15](https://contribute.qt-project.org/quips/15) +before sending an email to `security at qt-project dot org`. diff --git a/build_history/blacklist.txt b/build_history/blacklist.txt index 5f0183f5..adfb0c60 100644 --- a/build_history/blacklist.txt +++ b/build_history/blacklist.txt @@ -10,10 +10,6 @@ win32 ci linux darwin -[QtMultimedia::audio_test] - linux - darwin - win32 # Cannot create metal surface [QtMultimediaWidgets::qmultimediawidgets] darwin ci @@ -37,14 +33,9 @@ # Open GL functions failures on macOS (2/2020) [QtQml::qqmlnetwork_test] linux ci # extended, see PyPy section below -[QtWidgets::bug_750] - darwin ci -[QtWidgets::qpicture_test] - darwin ci [QtAsyncio::qasyncio_test_chain] win32 [QtQml::bug_825] - py3.8 # bug in typeobject::type_mro_modified, fix in 3.9 py3.9.0 py3.9.1 py3.9.2 diff --git a/build_scripts/config.py b/build_scripts/config.py index f11fbb5e..efafc768 100644 --- a/build_scripts/config.py +++ b/build_scripts/config.py @@ -78,9 +78,9 @@ class Config(metaclass=Singleton): self.setup_kwargs['long_description_content_type'] = 'text/markdown' self.setup_kwargs['keywords'] = _pyproject_data["keywords"] - _author, _email = _pyproject_data["authors"][0] - self.setup_kwargs['author'] = _author - self.setup_kwargs['author_email'] = _email + _author = _pyproject_data["authors"][0] + self.setup_kwargs['author'] = _author["name"] + self.setup_kwargs['author_email'] = _author["email"] self.setup_kwargs['url'] = _pyproject_data["urls"]["Homepage"] self.setup_kwargs['license'] = _pyproject_data["license"]["text"] self.setup_kwargs['python_requires'] = _pyproject_data["requires-python"] diff --git a/build_scripts/main.py b/build_scripts/main.py index 62602ef4..57e337ac 100644 --- a/build_scripts/main.py +++ b/build_scripts/main.py @@ -139,8 +139,11 @@ def check_allowed_python_version(): supported = get_allowed_python_versions() this_py = sys.version_info[:2] if this_py not in supported: - log.error(f"Unsupported python version detected. Supported versions: {supported}") - sys.exit(1) + log.warning("*" * 80) + log.warning(f"Unsupported Python version detected: {this_py}.") + log.warning("The build will probably fail.") + log.warning(f"Supported versions: {supported}") + log.warning("*" * 80) qt_src_dir = '' diff --git a/build_scripts/platforms/unix.py b/build_scripts/platforms/unix.py index 35d6dd1c..04403ba0 100644 --- a/build_scripts/platforms/unix.py +++ b/build_scripts/platforms/unix.py @@ -236,7 +236,7 @@ def prepare_packages_posix(pyside_build, _vars, cross_build=False): # Some libraries specific to Linux/Android from 6.8 # eg: the libav* libraries are required for the multimedia module - if config.is_internal_pyside_build() and sys.platform != "darwin": + if config.is_internal_pyside_build() and (sys.platform != "darwin" or is_android): qt_multimedia_filters = [f"lib{lib}*.so*" for lib in PYSIDE_MULTIMEDIA_LIBS] copydir("{qt_lib_dir}", destination_qt_dir / "lib", _filter=qt_multimedia_filters, diff --git a/build_scripts/platforms/windows_desktop.py b/build_scripts/platforms/windows_desktop.py index eaab7acb..073f390a 100644 --- a/build_scripts/platforms/windows_desktop.py +++ b/build_scripts/platforms/windows_desktop.py @@ -5,6 +5,7 @@ from __future__ import annotations import functools import os import tempfile +import platform from pathlib import Path @@ -12,7 +13,8 @@ from ..log import log from ..config import config from ..options import OPTION from ..utils import (copydir, copyfile, copy_qt_metatypes, - download_and_extract_7z, filter_match, makefile, in_coin) + download_and_extract_7z, filter_match, makefile, in_coin, + coin_job_id) from .. import PYSIDE, SHIBOKEN, PYSIDE_WINDOWS_BIN_TOOLS, PYSIDE_MULTIMEDIA_LIBS @@ -194,11 +196,11 @@ def prepare_packages_win32(pyside_build, _vars): if config.is_internal_shiboken_module_build(): # The C++ std library dlls need to be packaged with the # shiboken module, because libshiboken uses C++ code. - copy_msvc_redist_files(destination_dir) + download_qt_dependency_dlls(_vars, destination_dir, msvc_redist) if config.is_internal_pyside_build() or config.is_internal_shiboken_generator_build(): copy_qt_artifacts(pyside_build, destination_qt_dir, copy_pdbs, _vars) - copy_msvc_redist_files(destination_dir) + download_qt_dependency_dlls(_vars, destination_dir, msvc_redist) # MSVC redistributable file list. @@ -216,31 +218,20 @@ msvc_redist = [ ] -def copy_msvc_redist_files(destination_dir): - if not in_coin(): - log.info("Qt dependency DLLs (MSVC redist) will not be copied.") - return - - # Make a directory where the files should be extracted. - if not destination_dir.exists(): - destination_dir.mkdir(parents=True) +def get_cache_dir(): + """Return the name of a cache directory for storing artifacts for repetitive + runs of setup.py depending on COIN_UNIQUE_JOB_ID.""" + job_id = coin_job_id() + dir = tempfile.gettempdir() + os.sep + "pyside" + job_id[0:job_id.find('-')] + return Path(dir) - # Copy Qt dependency DLLs (MSVC) from PATH when building on Qt CI. - paths = os.environ["PATH"].split(os.pathsep) - for path in paths: - try: - for f in Path(path).glob("*140*.dll"): - if f.name in msvc_redist: - copyfile(f, Path(destination_dir) / f.name) - msvc_redist.remove(f.name) - if not msvc_redist: - break - except WindowsError: - continue - if msvc_redist: - msg = "The following Qt dependency DLLs (MSVC redist) were not found: {msvc_redist}" - raise FileNotFoundError(msg) +def download_and_extract_7z_to_cache(url, cache_dir): + """Download the artifacts to the cache directory unless it exists.""" + if not cache_dir.is_dir(): + cache_dir.mkdir(parents=True) + if not list(cache_dir.glob("*.dll")): + download_and_extract_7z(url, cache_dir) def download_qt_dependency_dlls(_vars, destination_qt_dir, artifacts): @@ -249,17 +240,19 @@ def download_qt_dependency_dlls(_vars, destination_qt_dir, artifacts): log.info("Qt dependency DLLs will not be downloaded and extracted.") return - with tempfile.TemporaryDirectory() as temp_path: - redist_url = "https://download.qt.io/development_releases/prebuilt/vcredist/" - zip_file = "pyside_qt_deps_681_64_2022.7z" - try: - download_and_extract_7z(redist_url + zip_file, temp_path) - except Exception as e: - log.warning(f"Download failed: {type(e).__name__}: {e}") - log.warning("download.qt.io is down, try with mirror") - redist_url = "https://master.qt.io/development_releases/prebuilt/vcredist/" - download_and_extract_7z(redist_url + zip_file, temp_path) - copydir(temp_path, destination_qt_dir, _filter=artifacts, recursive=False, _vars=_vars) + cache_dir = get_cache_dir() + redist_url = "https://download.qt.io/development_releases/prebuilt/vcredist/" + zip_file = "pyside_qt_deps_684_64_2022.7z" + if platform.machine() == "ARM64": + zip_file = "pyside_qt_deps_690_arm_2022.7z" + try: + download_and_extract_7z_to_cache(redist_url + zip_file, cache_dir) + except Exception as e: + log.warning(f"Download failed: {type(e).__name__}: {e}") + log.warning("download.qt.io is down, try with mirror") + redist_url = "https://master.qt.io/development_releases/prebuilt/vcredist/" + download_and_extract_7z_to_cache(redist_url + zip_file, cache_dir) + copydir(cache_dir, destination_qt_dir, _filter=artifacts, recursive=False, _vars=_vars) def copy_qt_artifacts(pyside_build, destination_qt_dir, copy_pdbs, _vars): @@ -444,5 +437,6 @@ def copy_qt_artifacts(pyside_build, destination_qt_dir, copy_pdbs, _vars): destination_qt_dir, _vars=_vars) - if copy_clang: + if copy_clang or platform.machine() == "ARM64": + # Qt CI is using dynamic libclang with arm config. pyside_build.prepare_standalone_clang(is_win=True) diff --git a/build_scripts/qfp_tool.py b/build_scripts/qfp_tool.py index 01c05f14..b1af14a4 100644 --- a/build_scripts/qfp_tool.py +++ b/build_scripts/qfp_tool.py @@ -37,6 +37,7 @@ Jobs Number of jobs to be run simultaneously Modules Comma separated list of modules to be built (for --module-subset=) Python Python executable (Use python_d for debug builds on Windows) +Wheel (boolean) Install via wheels instead of running setup.py install Arbitrary keys can be defined and referenced by $(name): @@ -76,6 +77,7 @@ GENERATOR_KEY = 'Generator' JOBS_KEY = 'Jobs' MODULES_KEY = 'Modules' PYTHON_KEY = 'Python' +WHEEL_KEY = 'Wheel' DEFAULT_MODULES = "Core,Gui,Widgets,Network,Test,Qml,Quick,Multimedia,MultimediaWidgets" DEFAULT_CONFIG_FILE = f"Modules={DEFAULT_MODULES}\n" @@ -282,10 +284,34 @@ def get_config_file(base_name) -> Path: return config_file -def build(target: str): - """Run configure and build steps""" - start_time = time.time() +def pip_list(): + """List installed packages from the output lines of pip (shiboken6 6.9.0a1).""" + result = [] + pattern = re.compile(r"^([^\s]+)\s+\d.*$") + for line in run_process_output(["pip", "list"]): + match = pattern.search(line) + if match: + result.append(match.group(1)) + return result + +def uninstall_pyside(): + """Uninstall all PySide related packages.""" + packages = [] + for p in pip_list(): + if "shiboken" in p or "PySide" in p: + packages.append(p) + if not packages or opt_dry_run: + return + yes = "Y\n" * len(packages) + cmd = ["pip", "uninstall"] + packages + with subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, + stderr=subprocess.PIPE, text=True) as process: + print(process.communicate(input=yes)[0]) + + +def run_build(target: str): + """Run configure and build steps""" arguments = [] acceleration = read_acceleration_config() if not IS_WINDOWS and acceleration == Acceleration.INCREDIBUILD: @@ -323,8 +349,33 @@ def build(target: str): execute(arguments) - elapsed_time = int(time.time() - start_time) - print(f'--- Done({elapsed_time}s) ---') + +def build(skip_install: bool): + """Run configure and build steps""" + start_time = time.time() + use_wheel = read_bool_config(WHEEL_KEY) + target = "build" if use_wheel or skip_install else "install" + run_build(target) + build_time_stamp = time.time() + elapsed_time = int(build_time_stamp - start_time) + print(f"--- Build done({elapsed_time}s) ---") + if not use_wheel or skip_install: + return + print() + wheel_dir = Path.cwd() / "dist" + if not opt_dry_run: + for w in wheel_dir.glob("*.whl"): + w.unlink() + create_wheel_cmd = [read_config_python_binary(), "create_wheels.py", "--no-examples"] + execute(create_wheel_cmd) + install_cmd = ["pip", "install", "--force-reinstall"] + for w in wheel_dir.glob("*.whl"): + if not w.name.startswith("pyside6-"): + install_cmd.append(os.fspath(w)) + execute(install_cmd) + install_time_stamp = time.time() + elapsed_time = int(install_time_stamp - build_time_stamp) + print(f"--- Install done({elapsed_time}s) ---") def build_base_docs(): @@ -373,6 +424,8 @@ def create_argument_parser(desc): help='Run tests') parser.add_argument('--Documentation', '-D', action='store_true', help='Run build_base_docs') + parser.add_argument('--uninstall', '-U', action='store_true', + help='Uninstall packages') parser.add_argument('--version', '-v', action='version', version='%(prog)s 1.0') parser.add_argument('--verbose', '-V', action='store_true', help='Turn off --quiet specified in build arguments') @@ -411,7 +464,8 @@ if __name__ == '__main__': build_mode = BuildMode.RECONFIGURE if build_mode == BuildMode.NONE and not (options.clean or options.reset or options.pull - or options.Documentation or options.test): + or options.uninstall or options.Documentation + or options.test): argument_parser.print_help() sys.exit(0) @@ -435,6 +489,9 @@ if __name__ == '__main__': base_dir = Path.cwd().name + if options.uninstall: + uninstall_pyside() + if options.clean: run_git(['clean', '-dxf']) @@ -445,8 +502,7 @@ if __name__ == '__main__': run_git(['pull', '--rebase']) if build_mode != BuildMode.NONE: - target = 'build' if options.no_install else 'install' - build(target) + build(options.no_install) if options.Documentation: build_base_docs() diff --git a/build_scripts/utils.py b/build_scripts/utils.py index 29f2545d..eb0c8b0b 100644 --- a/build_scripts/utils.py +++ b/build_scripts/utils.py @@ -1123,8 +1123,12 @@ def copy_qt_metatypes(destination_qt_dir, _vars): recursive=False, _vars=_vars, force_copy_symlinks=True) +def coin_job_id(): + return os.environ.get("COIN_UNIQUE_JOB_ID", None) + + def in_coin(): - return os.environ.get('COIN_UNIQUE_JOB_ID', None) is not None + return coin_job_id() is not None def parse_modules(modules: str) -> str: diff --git a/build_scripts/wheel_files.py b/build_scripts/wheel_files.py index 7ee04a7b..e9b936f1 100644 --- a/build_scripts/wheel_files.py +++ b/build_scripts/wheel_files.py @@ -539,6 +539,11 @@ def module_QtQml() -> ModuleData: data.qml.extend(_qml) data.extra_files.append("qmllint*") + # adds qmllint plugins + json_data_qmllint = get_module_json_data("QmlCompiler") + qml_lint_plugins = get_module_plugins(json_data_qmllint) + data.plugins += qml_lint_plugins + data.extra_files.append("qmlformat*") data.extra_files.append("qmlls*") @@ -727,6 +732,8 @@ def module_Qt3DInput() -> ModuleData: def module_Qt3DLogic() -> ModuleData: data = ModuleData("3DLogic", qml=["Qt3D/Logic"]) + json_data = get_module_json_data("3DLogic") + data.plugins = get_module_plugins(json_data) return data @@ -761,10 +768,11 @@ def module_QtQuick3D() -> ModuleData: "libQt63DQuick", "libQt63DQuickAnimation", "libQt63DQuickExtras", - "libQt63DQuickExtras", + "libQt63DQuickLogic", "libQt63DQuickInput", "libQt63DQuickRender", "libQt63DQuickScene2D", + "libQt63DQuickScene3D", "libQt6Quick3DXr", ] @@ -1032,6 +1040,7 @@ def module_QtVirtualKeyboard() -> ModuleData: data = ModuleData("VirtualKeyboard") data.plugins.append("virtualkeyboard") data.qtlib.append("libQt6VirtualKeyboardSettings") + data.qtlib.append("libQt6VirtualKeyboardQml") return data diff --git a/coin/dependencies.yaml b/coin/dependencies.yaml index 478bdfdf..ad4751d2 100644 --- a/coin/dependencies.yaml +++ b/coin/dependencies.yaml @@ -1,6 +1,6 @@ product_dependency: ../../qt/qt5: - ref: "480041bb0bfd400f29dd49facfaa924dacac5c03" + ref: "af7939f2df81369a4a6501c9b41e6e68278269eb" dependency_source: supermodule dependencies: [ "../../qt/qt3d", diff --git a/coin/fetch_libclang_arm64.ps1 b/coin/fetch_libclang_arm64.ps1 new file mode 100644 index 00000000..fcb4d711 --- /dev/null +++ b/coin/fetch_libclang_arm64.ps1 @@ -0,0 +1,8 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +# Download the file + +wget https://master.qt.io/development_releases/prebuilt/libclang/libclang-release_19.1.0-based-windows-vs2022_arm64.7z -o libclang.7z +# Unzip the contents to /home/qt +7z x libclang.7z -o/utils +Remove-Item libclang.7z diff --git a/coin/fetch_libclang_arm64.sh b/coin/fetch_libclang_arm64.sh index c99e3357..5de4a326 100644 --- a/coin/fetch_libclang_arm64.sh +++ b/coin/fetch_libclang_arm64.sh @@ -2,7 +2,7 @@ # Copyright (C) 2024 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only # Download the file -wget -q https://download.qt.io/development_releases/prebuilt/libclang/libclang-release_18.1.7-based-linux-Debian-11.6-gcc10.2-arm64.7z +wget -q https://master.qt.io/development_releases/prebuilt/libclang/libclang-release_18.1.7-based-linux-Debian-11.6-gcc10.2-arm64.7z if [ $? -ne 0 ]; then echo "Error: Failed to download libclang archive" >&2 exit 1 diff --git a/coin/instructions/common_environment.yaml b/coin/instructions/common_environment.yaml index 7663014e..949eecad 100644 --- a/coin/instructions/common_environment.yaml +++ b/coin/instructions/common_environment.yaml @@ -46,6 +46,33 @@ instructions: - condition: property property: host.os equals_value: Windows + - condition: property + property: host.arch + not_equals_value: AARCH64 + - type: EnvironmentVariable + variableName: PYTHON3_PATH + variableValue: "{{ index .Env \"PYTHON3.11.9-64_PATH\"}}" + enable_if: + condition: and + conditions: + - condition: property + property: host.os + equals_value: Windows + - condition: property + property: host.arch + equals_value: AARCH64 + - type: EnvironmentVariable + variableName: TARGET_ARCHITECTURE + variableValue: arm64 + enable_if: + condition: and + conditions: + - condition: property + property: target.arch + equals_value: AARCH64 + - condition: property + property: host.os + equals_value: Windows - type: EnvironmentVariable variableName: TARGET_ARCHITECTURE variableValue: amd64_x86 @@ -123,7 +150,7 @@ instructions: equals_value: Windows - type: PrependToEnvironmentVariable variableName: PATH - variableValue: "{{.Env.PYTHON3_PATH}}/bin:" + variableValue: "{{.Env.PYTHON3_PATH}}:" enable_if: condition: property property: host.os @@ -170,6 +197,18 @@ instructions: condition: property property: target.compiler equals_value: ICC_18 + - type: EnvironmentVariable + variableName: PYTHON3_PATH + variableValue: "{{ index .Env \"PYTHON3.10.0-64_PATH\"}}" + enable_if: + condition: and + conditions: + - condition: property + property: host.os + equals_value: Windows + - condition: property + property: host.arch + equals_value: X86_64 - type: EnvironmentVariable variableName: ICC64_18_PATH # Seems a bit hard to maintain variableValue: /opt/intel/compilers_and_libraries_2018.1.163/linux/bin/intel64:/opt/intel/bin @@ -236,6 +275,21 @@ instructions: equals_value: AARCH64 userMessageOnFailure: > Failed to download libclang from Qt servers + - type: ExecuteCommand + command: ["powershell", "-ExecutionPolicy", "Bypass", "-File", "coin\\fetch_libclang_arm64.ps1"] + maxTimeInSeconds: 14400 + maxTimeBetweenOutput: 1200 + enable_if: + condition: and + conditions: + - condition: property + property: host.os + equals_value: Windows + - condition: property + property: host.arch + equals_value: AARCH64 + userMessageOnFailure: > + Failed to download libclang from Qt servers - type: EnvironmentVariable variableName: LLVM_INSTALL_DIR variableValue: "/home/qt/libclang" @@ -248,6 +302,18 @@ instructions: - condition: property property: host.os equals_value: Linux + - type: EnvironmentVariable + variableName: LLVM_INSTALL_DIR + variableValue: "\\utils\\libclang" + enable_if: + condition: and + conditions: + - condition: property + property: target.arch + equals_value: AARCH64 + - condition: property + property: host.os + equals_value: Windows - type: EnvironmentVariable variableName: interpreter variableValue: "python3.11" diff --git a/coin/instructions/execute_desktop_instructions.yaml b/coin/instructions/execute_desktop_instructions.yaml index bece46b2..9bd113a2 100644 --- a/coin/instructions/execute_desktop_instructions.yaml +++ b/coin/instructions/execute_desktop_instructions.yaml @@ -47,6 +47,18 @@ instructions: condition: property property: host.os equals_value: Windows + - type: EnvironmentVariable + variableName: PYSIDE_SIGNING_DIR + variableValue: "{{.AgentWorkingDir}}\\pyside\\{{.Env.TESTED_MODULE_COIN}}\\build\\qfpa-p3.11\\package_for_wheels" + enable_if: + condition: and + conditions: + - condition: property + property: host.os + equals_value: Windows + - condition: property + property: host.arch + equals_value: AARCH64 - type: ExecuteCommand command: "{{.Env.interpreter}} -m pip install -r requirements-coin.txt --user" maxTimeInSeconds: 14400 @@ -83,7 +95,7 @@ instructions: userMessageOnFailure: > Failed to install requirements-coin.txt dependencies on Windows - type: ExecuteCommand - command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -u coin_build_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} {{.Env.CI_USE_SCCACHE}} --instdir=\\Users\\qt\\work\\install --targetOs={{.Env.CI_OS}} --hostArch=X86_64 --targetArch={{.Env.CI_TARGET_ARCHITECTURE}} --phase=BUILD" + command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -u coin_build_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} {{.Env.CI_USE_SCCACHE}} --instdir=\\Users\\qt\\work\\install --targetOs={{.Env.CI_OS}} --hostArch={{.Env.HOST_ARCH_COIN}} --targetArch={{.Env.TARGET_ARCH_COIN}} --phase=BUILD" maxTimeInSeconds: 14400 maxTimeBetweenOutput: 600 enable_if: diff --git a/coin/instructions/execute_test_instructions.yaml b/coin/instructions/execute_test_instructions.yaml index 7399ccb4..521503c3 100644 --- a/coin/instructions/execute_test_instructions.yaml +++ b/coin/instructions/execute_test_instructions.yaml @@ -86,7 +86,7 @@ instructions: userMessageOnFailure: > Failed to install requirements-coin.txt on Windows - type: ExecuteCommand - command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -u coin_test_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} --instdir=c:\\Users\\qt\\work\\install --targetOs={{.Env.CI_OS}} --hostArch=X86_64 --targetArch={{.Env.CI_TARGET_ARCHITECTURE}}" + command: "c:\\users\\qt\\MSVC.bat {{.Env.PYTHON3_PATH}}\\python.exe -u coin_test_instructions.py --os={{.Env.CI_OS}} {{.Env.CI_PACKAGING_FEATURE}} --instdir=c:\\Users\\qt\\work\\install --targetOs={{.Env.CI_OS}} --hostArch={{.Env.HOST_ARCH_COIN}} --targetArch={{.Env.TARGET_ARCH_COIN}}" maxTimeInSeconds: 14400 maxTimeBetweenOutput: 600 enable_if: diff --git a/coin/instructions/relocate_pyside.yaml b/coin/instructions/relocate_pyside.yaml index afab83c7..5e16aef5 100644 --- a/coin/instructions/relocate_pyside.yaml +++ b/coin/instructions/relocate_pyside.yaml @@ -31,7 +31,7 @@ instructions: userMessageOnFailure: > Failed to remove pyside-setup dir - type: InstallBinaryArchive - relativeStoragePath: "{{.Env.MODULE_ARTIFACTS_RELATIVE_STORAGE_PATH}}/artifacts.tar.gz" + relativeStoragePath: "{{.Env.MODULE_ARTIFACTS_RELATIVE_STORAGE_PATH}}/artifacts.tar.zst" directory: "pyside" maxTimeInSeconds: 1200 maxTimeBetweenOutput: 1200 diff --git a/coin/instructions_utils.py b/coin/instructions_utils.py index 176a6d22..52c8211e 100644 --- a/coin/instructions_utils.py +++ b/coin/instructions_utils.py @@ -144,7 +144,7 @@ def remove_variables(vars): del os.environ[env_var] -def setup_virtualenv(python, exe, env, pip, log): +def setup_virtualenv(python, exe, env, pip, log, ci): # Within Ubuntu 24.04 one can't install anything with pip to outside of # virtual env. Trust that we already have proper virtualenv installed. if os.environ.get("HOST_OSVERSION_COIN") != "ubuntu_24_04": @@ -156,7 +156,10 @@ def setup_virtualenv(python, exe, env, pip, log): env_path = Path(str(site.USER_BASE)) / "bin" v_env = env_path / "virtualenv" if sys.platform == "win32": - env_path = os.path.join(site.USER_BASE, "Scripts") + if ci.TARGET_ARCH == "aarch64": + env_path = os.path.join(site.USER_BASE, "Python311-arm64", "Scripts") + else: + env_path = os.path.join(site.USER_BASE, "Scripts") v_env = os.path.join(env_path, "virtualenv.exe") try: run_instruction([str(v_env), "--version"], "Using default virtualenv") @@ -191,7 +194,7 @@ def call_setup(python_ver, ci, phase, log, buildnro=0): python = Path(get_env_or_raise("PYTHON3_PATH")) / "python.exe" if phase == "BUILD": - setup_virtualenv(python, exe, env, pip, log) + setup_virtualenv(python, exe, env, pip, log, ci) elif phase == "TEST": if ci.HOST_OS == "MacOS" and ci.HOST_ARCH == "ARM64": @@ -201,7 +204,7 @@ def call_setup(python_ver, ci, phase, log, buildnro=0): [pip, "install", "-r", "requirements.txt"], "Failed to install dependencies" ) else: - setup_virtualenv(python, exe, env, pip, log) + setup_virtualenv(python, exe, env, pip, log, ci) # Install distro to replace missing platform.linux_distribution() in python3.8 run_instruction([pip, "install", "distro"], "Failed to install distro") diff --git a/coin/module_config.yaml b/coin/module_config.yaml index 450365ca..471c8403 100644 --- a/coin/module_config.yaml +++ b/coin/module_config.yaml @@ -25,9 +25,6 @@ accept_configuration: - condition: property # Windows on Arm property: target.arch not_equals_value: ARM64 - - condition: property # Windows on Arm host build - property: target.arch - not_equals_value: AARCH64 - condition: property property: features not_contains_value: DebianPackaging diff --git a/create_wheels.py b/create_wheels.py index 32a89a6a..55d02928 100644 --- a/create_wheels.py +++ b/create_wheels.py @@ -149,8 +149,8 @@ def get_platform_tag() -> str: # We know the CI builds universal2 wheels _tag = f"macosx_{target}_universal2" elif _os == "win32": - win_arch = platform.architecture()[0] - msvc_arch = "x86" if win_arch.startswith("32") else "amd64" + win_arch = platform.machine() + msvc_arch = "arm64" if win_arch.startswith("ARM64") else "amd64" _tag = f"win_{msvc_arch}" return _tag diff --git a/doc/changelogs/changes-6.8.3 b/doc/changelogs/changes-6.8.3 new file mode 100644 index 00000000..05b90abb --- /dev/null +++ b/doc/changelogs/changes-6.8.3 @@ -0,0 +1,52 @@ +Qt for Python 6.8.3 is a bug-fix release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* PySide6 * +**************************************************************************** + + - [PYSIDE-1735] Most enumerations are now fully qualified in documentation + and translated code snippets. Enum values automatically + converted to "None_" are also correctly documented. + - [PYSIDE-2846] The type annotation of the Slot() decorator has been fixed. + - [PYSIDE-2997] Type hints: The signature of QMessageBox.information() + has been fixed. + - [PYSIDE-3001] QtAsyncio: The error messages printed on an exceptions + in tasks have been improved. + - [PYSIDE-3002] QPaintEngine's virtual functions taking a + C-style array of geometry primitives have been fixed. + - [PYSIDE-3003] Type hints: A bug causing overloads to be omitted has + been fixed. + - [PYSIDE-3012] Type hints: Since using Callable, Iterable and Sequence from + typing is deprecated, they are imported from collections.abc. + - [PYSIDE-3012] Type hints: Object inheritance has been removed from classes. + - [PYSIDE-3013] QFont.Tag(str), QFont.Tag.fromString(), QFont.Tag.fromValue() + have been added. + - [PYSIDE-3014] QAbstractSpinBox.fixup() has been fixed. + - [PYSIDE-3017] The overloads of QCanDbcFileParser.parse() have been fixed. + - [PYSIDE-3020] A bug disconnecting a string-based connection by passing a + callable has been fixed. + - [QTBUG-72968] The type of the "result" parameter of the native event + filters has been changed to "qintptr" for Qt 6. + +**************************************************************************** +* Shiboken6 * +**************************************************************************** + + - Warnings about rejected functions/fields have been removed or redirected + to the log files. + - [PYSIDE-2701] The size of the generated modules has been reduced by + factoring out common code from the code generated for + virtual functions. diff --git a/doc/changelogs/changes-6.9.0 b/doc/changelogs/changes-6.9.0 new file mode 100644 index 00000000..f706b205 --- /dev/null +++ b/doc/changelogs/changes-6.9.0 @@ -0,0 +1,98 @@ +Qt for Python 6.9.0 is a minor release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* PySide6 * +**************************************************************************** + + - The usage of macros from CPython (limited API) has been reduced in favor + of calling functions directly. + - Support for std::chrono::milliseconds has been added. + - Windows ARM64 was added as a new supported platform (technical preview) + - [PYSIDE-862] Support for QtRemoteObjects has been extended. + - [PYSIDE-1057] A memory leak connecting to signals has been fixed. + - [PYSIDE-1277] A bug connecting signals by QMetaMethod has been fixed. + - [PYSIDE-1735] Most enumerations are now fully qualified in documentation + and translated code snippets. Enum values automatically + converted to "None_" are also correctly documented. + - [PYSIDE-2714] Qt Creator 17.x+ and PySide tools now support projects that + have a pyproject.toml instead of a *.pyproject + - [PYSIDE-2846] The type annotation of the Slot() decorator has been fixed. + - [PYSIDE-2966] A bug making it possible to instantiate non-constructible + classes and namespaces has been fixed. + - [PYSIDE-2891] A bug in signature handling affecting 32bit builds + has been fixed. + - [PYSIDE-2958] Building against unsupported python versions has been + enabled. + - [PYSIDE-2997] Type hints: The signature of QMessageBox.information() + has been fixed. + - [PYSIDE-3001] QtAsyncio: The error messages printed on an exceptions + in tasks have been improved. + - [PYSIDE-3002] QPaintEngine's virtual functions taking a + C-style array of geometry primitives have been fixed. + - [PYSIDE-3003] Type hints: A bug causing overloads to be omitted has + been fixed. + - [PYSIDE-3004] A crash in slots when receiving an object-type parameter + passed by const-ref has been fixed. + - [PYSIDE-3005] A bug affecting + QOpenGLShaderProgram.setUniformValueArray(int,float*,int,int) + has been fixed. + - [PYSIDE-3012] Type hints: Since using Callable, Iterable and Sequence from + typing is deprecated, they are imported from collections.abc. + - [PYSIDE-3012] Type hints: Object inheritance has been removed from classes. + - [PYSIDE-3013] QFont.Tag(str), QFont.Tag.fromString(), QFont.Tag.fromValue() + have been added. + - [PYSIDE-3014] QAbstractSpinBox.fixup() has been fixed. + - [PYSIDE-3017] The overloads of QCanDbcFileParser.parse() have been fixed. + - [PYSIDE-3020] A bug disconnecting a string-based connection by passing a + callable has been fixed. + - [QTBUG-72968] The type of the "result" parameter of the native event + filters has been changed to "qintptr" for Qt 6. + - [QTBUG-108199] PySide has been adapted to the deprecation of Qt::TimeSpec + in Qt. + +**************************************************************************** +* Shiboken6 * +**************************************************************************** + + - Helper class AutoArrayPointer has been renamed to ArrayPointer. A + convenience typedef is provided. + - It is now possible to use file snippets for XML template content. + - Warnings about rejected functions/fields have been removed or redirected + to log files to reduce clutter. + - An additional log file, mjb_shiboken.log has been introduced which + contains informational messages about the types encountered. + - [PYSIDE-454] It is now possible to exclude smart pointer instantiations + from underlying base modules to prevent symbol clashes. + - [PYSIDE-2701] The size of the generated modules has been reduced by + applying several optimizations to the code generated for + virtual functions. + - [PYSIDE-2701] Function modifications are now correctly inherited also in + case of multiple inheritance. + - [PYSIDE-2986] shiboken6 can now generate doc strings for classes from + injected documentation snippets. + - [PYSIDE-3004] The detection of copy constructibility of value type + classes has been improved using functionality from Clang. + New typesystem attributes have been introduced to enable + overriding the detection. + - [PYSIDE-3004] The handling of move only value types has been improved. + - [PYSIDE-3004] A documentation page about value versus objects has been + added. + - [PYSIDE-3004] Non-default constructible value types can now be + passed by non-const reference. + - [PYSIDE-3004] Warnings about special types are now printed, particularly + about object types that could be value types. + - [QTBUG-133704] It is now possible to specify in the type system whether + Qt meta type registration code should be generated for enums. diff --git a/doc/changelogs/changes-6.9.1 b/doc/changelogs/changes-6.9.1 new file mode 100644 index 00000000..6f4ef508 --- /dev/null +++ b/doc/changelogs/changes-6.9.1 @@ -0,0 +1,64 @@ +Qt for Python 6.9.1 is a bug-fix release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* PySide6 * +**************************************************************************** + + - [PYSIDE-841] An example showing an audio graph using Qt Graphs has been + added. + - [PYSIDE-2193] A crash retrieving a Python type QObject property + (encapsulated in a QVariant) been fixed. + - [PYSIDE-3052] class QMessageLogger has been added for handling debug, + info, warning, critical, and fatal messages, + including support for QLoggingCategory. + - [PYSIDE-3012] type hints: The type signature for the Property class + has been fixed. + - [PYSIDE-3021] type-hints: The QMessagebox annotations has been fixed. + - [PYSIDE-3029] type-hints: The hints for properties on __init__ functions + has been fixed. + - [PYSIDE-3041] type hints: The str parameter of QLineEdit.setText() has + been made optional. + - [PYSIDE-3050] type hints: Type signature for + QProgressDialog.setCancelButton has been fixed. + - [PYSIDE-3055] type hints: Type signature for QTabBar.setTabButton has + been fixed. + - [PYSIDE-3056] type hints: Type signature for qtTrId has been fixed. + - [PYSIDE-3057] type hints: Type signature for QTreeWidget.setItemWidget + has been fixed. + - [PYSIDE-3058] type hints: Return value for QTreeWidget.topLevelItem and + QTreeWidget.takeTopLevelItem has been fixed. + - [PYSIDE-3059] type hints: The type signature for QObject class has been + fixed. + - [PYSIDE-3061] The building of .pyi files in debug mode on Windows has + been fixed. + - [PYSIDE-3067] A crash when entering a Qt message handler with a Python + error set has been fixed. + - [PYSIDE-3069] A crash retrieving a QGraphicsProxyObject from a QVariant + has been fixed. + - [PYSIDE-3078] type hints: The parent widget parameter of the + QInputDialog get() methods has been made optional. + - [PYSIDE-3087] The dependency of pyside6-project on tomlkit has been + removed. + - [PYSIDE-3089] An error in pyside6-metaobjectdump when encountering + @Slot(result=None) has been fixed. + +**************************************************************************** +* Shiboken6 * +**************************************************************************** + + - [PYSIDE-3081] A bug in the clang parser causing errors when parsing a + lambda contained in a function parameter default value has + been fixed. diff --git a/doc/changelogs/changes-6.9.2 b/doc/changelogs/changes-6.9.2 new file mode 100644 index 00000000..bfbee9ad --- /dev/null +++ b/doc/changelogs/changes-6.9.2 @@ -0,0 +1,59 @@ +Qt for Python 6.9.2 is a bug-fix release. + +For more details, refer to the online documentation included in this +distribution. The documentation is also available online: + +https://doc.qt.io/qtforpython/ + +Some of the changes listed in this file include issue tracking numbers +corresponding to tasks in the Qt Bug Tracker: + +https://bugreports.qt.io/ + +Each of these identifiers can be entered in the bug tracker to obtain more +information about a particular change. + +**************************************************************************** +* PySide6 * +**************************************************************************** + + - [PYSIDE-1612] Android Deployment: Installing packages is no longer forced. + - [PYSIDE-1612] Desktop Deployment: A warning for Qt resource files has + been fixed. + - [PYSIDE-1612] Desktop Deployment: Nuitka has been upgraded to 2.7.11. + - [PYSIDE-2846] type hints: The mypy version has been increased. + - [PYSIDE-2846] type hints: Signature warnings about QObject properties of + unknown type passed to the constructor have been fixed. + - [PYSIDE-2938] The MSVC runtime libraries bundled in Windows wheels + have been updated. + - [PYSIDE-3095] pyside6-project lupdate now supports specifying + subdirectories in .ts file names. + - [PYSIDE-3115] A bug affecting QVariant conversion of Python classes + inheriting QGraphicsProxyObject has been fixed. + - [PYSIDE-3119] A SECURITY.md document required for Github has been added. + - [PYSIDE-3124] Documentation about thread affinity has been added + to the Signals and Slot tutorial. + - [PYSIDE-3127] A bug occurring when choosing a camera in the camera + example has been fixed. + - [PYSIDE-3132] A crash calling setItemDelegateForColumn/Row() + repeatedly has been fixed. + - [PYSIDE-3133] A crash on conversion errors when parsing keyword + arguments has been fixed. + - [PYSIDE-3135] type hints: typing.Self is no longer modified in + versions < 3.11. + - [PYSIDE-3139] type hints: The return types of QGuiApplication.screenAt() + and QGuiApplication.modalWindow() have been fixed. + - [PYSIDE-3146] Deployment: Values generated into pysidedeploy.spec are + now sorted. + - [PYSIDE-3147] Initial adaptations for the upcoming Python version 3.14 + have been done. + - [PYSIDE-3148] A memory corruption occurring when connecting several + signals to one non-QObject receiver has been fixed. + +**************************************************************************** +* Shiboken6 * +**************************************************************************** + + - [PYSIDE-3105] Cross compilation support has been improved. + - [PYSIDE-3144] A crash occurring when no source class can be found for + typedef typesystem entries has been fixed. diff --git a/examples/charts/donutbreakdown/donutbreakdown.py b/examples/charts/donutbreakdown/donutbreakdown.py index d6792357..db239918 100644 --- a/examples/charts/donutbreakdown/donutbreakdown.py +++ b/examples/charts/donutbreakdown/donutbreakdown.py @@ -39,7 +39,7 @@ class MainSlice(QPieSlice): class DonutBreakdownChart(QChart): def __init__(self, parent=None): super().__init__(QChart.ChartTypeCartesian, - parent, Qt.WindowFlags()) + parent, Qt.WindowFlags(0)) self.main_series = QPieSeries() self.main_series.setPieSize(0.7) self.addSeries(self.main_series) diff --git a/examples/charts/dynamicspline/chart.py b/examples/charts/dynamicspline/chart.py index 0858007f..f02c1b88 100644 --- a/examples/charts/dynamicspline/chart.py +++ b/examples/charts/dynamicspline/chart.py @@ -11,7 +11,7 @@ from PySide6.QtGui import QPen class Chart(QChart): def __init__(self, parent=None): - super().__init__(QChart.ChartTypeCartesian, parent, Qt.WindowFlags()) + super().__init__(QChart.ChartTypeCartesian, parent, Qt.WindowFlags(0)) self._timer = QTimer() self._series = QSplineSeries(self) self._titles = [] diff --git a/examples/charts/lineandbar/lineandbar.py b/examples/charts/lineandbar/lineandbar.py index fe559a64..f1d375c8 100644 --- a/examples/charts/lineandbar/lineandbar.py +++ b/examples/charts/lineandbar/lineandbar.py @@ -58,7 +58,7 @@ class TestChart(QMainWindow): self._axis_x.setRange("Jan", "Jun") self._axis_y = QValueAxis() - self.chart.addAxis(self._axis_x, Qt.AlignLeft) + self.chart.addAxis(self._axis_y, Qt.AlignLeft) self._line_series.attachAxis(self._axis_y) self._bar_series.attachAxis(self._axis_y) self._axis_y.setRange(0, 20) diff --git a/examples/demos/documentviewer/doc/imageviewer.py.rstinc b/examples/demos/documentviewer/doc/imageviewer.py.rstinc new file mode 100644 index 00000000..2aeaaf4a --- /dev/null +++ b/examples/demos/documentviewer/doc/imageviewer.py.rstinc @@ -0,0 +1,11 @@ +``ImageViewer`` displays images as supported by ``QImageReader``, using +a QLabel. + +In the constructor, we increase the allocation limit of ``QImageReader`` to +allow for larger photos. + +In the ``openFile()`` function, we load the image and determine its size. +If it is larger than the screen, we downscale it to screen size, maintaining +the aspect ratio. This calculation has to be done in native pixels, and +the device pixel ratio needs to be set on the resulting pixmap for it to +appear crisp. diff --git a/examples/demos/documentviewer/documentviewer.pyproject b/examples/demos/documentviewer/documentviewer.pyproject index 461e3b9d..fe1a4dbf 100644 --- a/examples/demos/documentviewer/documentviewer.pyproject +++ b/examples/demos/documentviewer/documentviewer.pyproject @@ -4,6 +4,7 @@ "main.py", "mainwindow.py", "mainwindow.ui", + "imageviewer/imageviewer.py", "jsonviewer/jsonviewer.py", "pdfviewer/pdfviewer.py", "pdfviewer/zoomselector.py", diff --git a/examples/demos/documentviewer/imageviewer/imageviewer.py b/examples/demos/documentviewer/imageviewer/imageviewer.py new file mode 100644 index 00000000..a2155aca --- /dev/null +++ b/examples/demos/documentviewer/imageviewer/imageviewer.py @@ -0,0 +1,173 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +from __future__ import annotations + +import math + +from PySide6.QtWidgets import QLabel +from PySide6.QtCore import Qt, QDir, QSizeF +from PySide6.QtGui import (QPixmap, QImageReader, QIcon, QKeySequence, + QGuiApplication, QColorSpace, QPainter, QAction) + +from abstractviewer import AbstractViewer + + +def imageFormats(): + result = [] + all_formats = QImageReader.supportedImageFormats() + + for format_bytes in all_formats: + format_str = bytes(format_bytes).decode("utf-8") # Convert QByteArray to str + if format_str not in ["tif", "cur"]: # Exclude duplicate/non-existent formats + result.append(f"image/{format_str}") + + return result + + +def msgOpen(name, image): + description = image.colorSpace().description() if image.colorSpace().isValid() else "unknown" + return 'Opened "{0}", {1}x{2}, Depth: {3} ({4})'.format( + QDir.toNativeSeparators(name), + image.width(), + image.height(), + image.depth(), + description + ) + + +class ImageViewer(AbstractViewer): + + def __init__(self): + super().__init__() + + self.formats = imageFormats() + self.uiInitialized.connect(self.setupImageUi) + QImageReader.setAllocationLimit(1024) # MB + + def init(self, file, parent, mainWindow): + self.image_label = QLabel(parent) + self.image_label.setFrameShape(QLabel.Box) + self.image_label.setAlignment(Qt.AlignCenter) + self.image_label.setScaledContents(True) + + # AbstractViewer.init(file, self.image_label, mainWindow) + super().init(file, self.image_label, mainWindow) + + self.tool_bar = self.addToolBar(self.tr("Images")) + + icon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomIn, + QIcon(":/demos/documentviewer/images/zoom-in.png")) + self.zoom_in_act = QAction(icon, "Zoom &In", self) + self.zoom_in_act.setShortcut(QKeySequence.StandardKey.ZoomIn) + self.zoom_in_act.triggered.connect(self.zoomIn) + self.tool_bar.addAction(self.zoom_in_act) + + icon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomOut, + QIcon(":/demos/documentviewer/images/zoom-out.png")) + self.zoom_out_act = QAction(icon, "Zoom &Out", self) + self.zoom_out_act.setShortcut(QKeySequence.StandardKey.ZoomOut) + self.zoom_out_act.triggered.connect(self.zoomOut) + self.tool_bar.addAction(self.zoom_out_act) + + icon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomFitBest, + QIcon(":/demos/documentviewer/images/zoom-fit-best.png")) + self.reset_zoom_act = QAction(icon, "Reset Zoom", self) + self.reset_zoom_act.setShortcut(QKeySequence + (Qt.KeyboardModifier.ControlModifier | Qt.Key.Key_0)) + self.reset_zoom_act.triggered.connect(self.resetZoom) + self.tool_bar.addAction(self.reset_zoom_act) + + def supportedMimeTypes(self): + return self.formats + + def clear(self): + self.image_label.setPixmap(QPixmap()) + self.max_scale_factor = self.min_scale_factor = 1 + self.initial_scale_factor = self.scale_factor = 1 + + def setupImageUi(self): + self.openFile() + + def openFile(self): + + QGuiApplication.setOverrideCursor(Qt.WaitCursor) + + name = self._file.fileName() + reader = QImageReader(name) + orig_image = reader.read() + + if orig_image.isNull(): + self.statusMessage(f"Cannot read file {name}:\n{reader.errorString()}", "open") + self.disablePrinting() + QGuiApplication.restoreOverrideCursor() + return + + self.clear() + + if orig_image.colorSpace().isValid(): + image = orig_image.convertedToColorSpace(QColorSpace.SRgb) + else: + image = orig_image + + device_pixel_ratio = self.image_label.devicePixelRatioF() + self.image_size = QSizeF(image.size()) / device_pixel_ratio + + pixmap = QPixmap.fromImage(image) + pixmap.setDevicePixelRatio(device_pixel_ratio) + self.image_label.setPixmap(pixmap) + + target_size = self.image_label.parentWidget().size() + if (self.image_size.width() > target_size.width() + or self.image_size.height() > target_size.height()): + self.initial_scale_factor = min(target_size.width() / self.image_size.width(), + target_size.height() / self.image_size.height()) + + self.max_scale_factor = 3 * self.initial_scale_factor + self.min_scale_factor = self.initial_scale_factor / 3 + self.doSetScaleFactor(self.initial_scale_factor) + + self.statusMessage(msgOpen(name, orig_image)) + QGuiApplication.restoreOverrideCursor() + + self.maybeEnablePrinting() + + def setScaleFactor(self, scaleFactor): + if not math.isclose(self.scale_factor, scaleFactor): + self.doSetScaleFactor(scaleFactor) + + def doSetScaleFactor(self, scaleFactor): + self.scale_factor = scaleFactor + label_size = (self.image_size * self.scale_factor).toSize() + self.image_label.setFixedSize(label_size) + self.enableZoomActions() + + def zoomIn(self): + self.setScaleFactor(self.scale_factor * 1.25) + + def zoomOut(self): + self.setScaleFactor(self.scale_factor * 0.8) + + def resetZoom(self): + self.setScaleFactor(self.initial_scale_factor) + + def hasContent(self): + return not self.image_label.pixmap().isNull() + + def enableZoomActions(self): + self.reset_zoom_act.setEnabled(not math.isclose(self.scale_factor, + self.initial_scale_factor)) + self.zoom_in_act.setEnabled(self.scale_factor < self.max_scale_factor) + self.zoom_out_act.setEnabled(self.scale_factor > self.min_scale_factor) + + def printDocument(self, printer): + if not self.hasContent(): + return + + painter = QPainter(printer) + pixmap = self.image_label.pixmap() + rect = painter.viewport() + size = pixmap.size() + size.scale(rect.size(), Qt.KeepAspectRatio) + painter.setViewport(rect.x(), rect.y(), size.width(), size.height()) + painter.setWindow(pixmap.rect()) + painter.drawPixmap(0, 0, pixmap) diff --git a/examples/demos/documentviewer/pdfviewer/pdfviewer.py b/examples/demos/documentviewer/pdfviewer/pdfviewer.py index a1f4e744..a2de67ad 100644 --- a/examples/demos/documentviewer/pdfviewer/pdfviewer.py +++ b/examples/demos/documentviewer/pdfviewer/pdfviewer.py @@ -67,7 +67,7 @@ class PdfViewer(AbstractViewer): actionZoomIn.setToolTip("Increase zoom level") actionZoomIn.triggered.connect(self.onActionZoomInTriggered) - icon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomIn, + icon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomOut, QIcon(":/demos/documentviewer/images/zoom-out.png")) actionZoomOut = self._toolBar.addAction(icon, "Zoom out", QKeySequence.StandardKey.ZoomOut) actionZoomOut.setToolTip("Decrease zoom level") diff --git a/examples/demos/documentviewer/viewerfactory.py b/examples/demos/documentviewer/viewerfactory.py index 0d32cbfe..19b9f6a3 100644 --- a/examples/demos/documentviewer/viewerfactory.py +++ b/examples/demos/documentviewer/viewerfactory.py @@ -10,6 +10,7 @@ from PySide6.QtCore import (QFileInfo, QMimeDatabase, QTimer) from txtviewer.txtviewer import TxtViewer from jsonviewer.jsonviewer import JsonViewer from pdfviewer.pdfviewer import PdfViewer +from imageviewer.imageviewer import ImageViewer class DefaultPolicy(Enum): @@ -29,7 +30,7 @@ class ViewerFactory: self._displayWidget = displayWidget self._mainWindow = mainWindow self._mimeTypes = [] - for v in [PdfViewer(), JsonViewer(), TxtViewer()]: + for v in [PdfViewer(), JsonViewer(), TxtViewer(), ImageViewer()]: self._viewers[v.viewerName()] = v if v.isDefaultViewer(): self._defaultViewer = v diff --git a/examples/graphs/2d/graphsaudio/GraphsAudio/Main.qml b/examples/graphs/2d/graphsaudio/GraphsAudio/Main.qml new file mode 100644 index 00000000..51bf3ef1 --- /dev/null +++ b/examples/graphs/2d/graphsaudio/GraphsAudio/Main.qml @@ -0,0 +1,50 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtGraphs + +ApplicationWindow { + visible: true + width: 1000 + height: 800 + title: "Data from the microphone (" + device_name + ")" + + GraphsView { + id: graph + anchors.fill: parent + + LineSeries { + id: audio_series + width: 2 + color: "#007acc" + } + + axisX: ValueAxis { + min: 0 + max: 2000 + tickInterval : 500 + labelFormat: "%g" + titleText: "Samples" + } + + axisY: ValueAxis { + min: -1 + max: 1 + tickInterval : 0.5 + labelFormat: "%0.1f" + titleText: "Audio level" + } + } + + Connections { + target: audio_bridge + function onDataUpdated(buffer) { + audio_series.clear() + for (let i = 0; i < buffer.length; ++i) { + audio_series.append(buffer[i]) + } + } + } +} diff --git a/examples/graphs/2d/graphsaudio/GraphsAudio/qmldir b/examples/graphs/2d/graphsaudio/GraphsAudio/qmldir new file mode 100644 index 00000000..cc5408a6 --- /dev/null +++ b/examples/graphs/2d/graphsaudio/GraphsAudio/qmldir @@ -0,0 +1,2 @@ +module GraphsAudio +Main 1.0 Main.qml diff --git a/examples/graphs/2d/graphsaudio/doc/graphsaudio.rst b/examples/graphs/2d/graphsaudio/doc/graphsaudio.rst new file mode 100644 index 00000000..f19b28ca --- /dev/null +++ b/examples/graphs/2d/graphsaudio/doc/graphsaudio.rst @@ -0,0 +1,8 @@ +GraphsAudio Example +=================== + +This example shows the drawing of dynamic data (microphone input) using QtGraphs and Qml. + +.. image:: graphsaudio.webp + :width: 400 + :alt: GraphsAudio Screenshot diff --git a/examples/graphs/2d/graphsaudio/doc/graphsaudio.webp b/examples/graphs/2d/graphsaudio/doc/graphsaudio.webp new file mode 100644 index 0000000000000000000000000000000000000000..bb57b18e5b8acf0fbbaef8bb53b9ea82897227b0 GIT binary patch literal 12908 zcmaKyb9kiNw(cvo-Elg$Ivv}#)3I%KY}n1pNsmVp7}jBzd7F; zV|>4NJT*&ROjy`}5dcsT;+IvHWhGGk98U%VWdKnggYbfI#|URilH~o#C5)q(qDO`@ zwz{@JkMGE$LUWKjNF(`4Mu2mlQGXMa^^x)pGd&UQj&j?v?77VCcEL1|Dc{=hOtvcX zj3Dn-!sYE%`w@9J^L{b=GZ|sUS_$DAzh-^k%fidlYr@0ZOZ>z5IPXJ$G3^E8MdrhE zTYHFS((B_s{$uE6>ay+a<_P4J?5W|A?wMz;W6W#x{p32}L*}`11i?{T z?PK>t<`Uzz=6T~a>+)mY%k$i5H(_?q;1c$nHfJ+w%ld3;UhJF=LK9(OBU zu6he{3JO9t(5_>=Mv!D7DdN$0)F|U@x1$ZByz25sk_sqq!+D5diI$Of^0Q4twPrWz zx2>RxMDSrbYdCi?rY+MWj)y?J=H_oDHm&giJXjSML#lAJbiem+9%&&A6mDr<{)I&W}9Kqk#=dmG4Ka?cKgGkR`yW+(M% zJ}3t|FxG18e{i&F-D#xUXr+Ht|5UvBEI%{td;!aio*CC+wqb2S$&eOi%$VPt$a%n% zBxM?rQ@Nr3<{tU&1c37!(dr=hG>bO&hi#l2Pi~VZhp9`0hf&aW<*kUyS|1(+m zGNc`<`~zCE6jt^ey5aAA0+@^9{w?5DEdnI2!$tS9_BP|C`(+^t$^9V^A4W#(Q{XLg z4aIxvyfGy|0E&y`d z$Nykb8RYuckkPM`dmSLkO8>R6f4+Dj)+WAH1JRsZ{6E9`yOecBzmI*{Pnn%i5a1wZ zo1oG0vALP-!;@dyN6i#_I6=>PgTog z)~4dRC&~-BdGr1ZN6aJSr&cj&27ynl0fg8liooSiuD@5#ObOGf8`BPJ^l2nr`MKMe zfXiQc5CV$wFV-BcFMIx}+y6=don;=+^H*Vaf0oJlPP^v8Y+2v^cVbx@VRRQ3pSHYAY$^q zt^fBL{wQcVpD|vQ`@iKDN5#Zw&aL9+Gnh4ELKI)s(LN;bDPt34;{Ge6BAZ5>=N42e zGa25y(y;u)CC7*(XkWPoI(Yet{r5}6ugXEyORM{SVG?A4XHuPjpJ?oIYV>=~`*#sS z!kycHR@2`*;tB}*Iko4SIgIo-hVX~0f^kyhpp{f1)ZW@HvE=`&EdHw&^w%w?J-?j_ zPfpf`HURC6SsEbc&VenOfX4oYl(#82DCxYyeEaU+Nb<%p+0PAhf8snb*M-Ij`c+~9 zm^q)h<&Tc2QA;5c?|NAqNZnP*n+`t=Lkg3QnpZdc6N@#k{UM}I$;fj=Zw|9Oi-!<|lu3L&D~N?Dk$`^xDNb-wDTeJ%@WpgfQ4&B^5=Y#Ct&3@T6zFgSGPa9+ zOS;aUqn)h{e)bb1Qme6ILkprLDDkxo5f*g-cZ^yh@eXF?s)T#Wtdv3c z;!|9JRHWIsPdp3661#>0^^&gZ)*toD%sT{PDch#c4>L=JT@V`|>< znO1eqf#P=pR6aY;xZAA!H?kuhVLB0a_J>>i?Rwiie?+-t0(XKo8J-j0jFEyt*gu>5 z&uqyHRBYA;I}gw!L`%djtGa_M0t3vEKHyD@aCEreQQ@Nsl|)cN(*FIFSqYWok+jv? z@0nsXay%>um3*(#+q!lM6O44asNU_IQE#$L%O1aqKm49Z>rQ>~1=#=2D{p*h(!nTP z6C^^DdH+NqG{T=Q=J^DGSRp1+8u2eeVf-fiBGh??J)!(PoRa3OPaMG{LY=vekkS`@ zT;KmDYfoTm2*mrvc_}~lKXCD1Pa_kaGYi^7&JlWms6L1q~LtNqCWtc2ZZ6kURT>CTq!{rsG|Vw3)IT;U)XOsjsL{rI)Dp28ak=U61EBD zld+ryUlW{{$Ui6iZ&9!V8DjxSnCx$@fM^??FG~JTsLmbMoHL+%1Q&y*iM%9ZDVheY z;ZCRh!$`7IE?{O`mRRF29$vuAx0mX!qFMg~&)>JE9@s8luKI<`NruktNpH;3RZrZ< zcM(CxvbDeQI`ipp|0XVl4U<4fZQS4H>!Lkad6mza_OH&tR)C^Irr+R){3rGQjq(38 z8g^fPv5EaG_vsf1wTJCQegD%r{$(QnN$CF;@}I=Dl)9P!pKu;Q(Lg5~JM1QY5~B0x zZSI%vnGJvK*YG=TaV}PG9`FC^d?EkX-Ogl&8uu;f$H{fV_&#-1q8yHRDd`@e>U5MR0cABnSId7Md$RsX7o zjp@*@dMKs%+imW}Kebu|@ccwv1gnmzOnuf~PR*4Hmiyp0OaHMt_OK;6?SC@!WqjxQ zj1rGn%2uDv&}UCl+)REO_pjch2f_a>@FU@NqHO@ZP!$BTqvehFU7s(zp?q?diQjOFZr zeY}#tdxMqEK-8~F5C5gf59E-;iSoy>i|*m%=Cgyyr$TA}vU1S}RNJ1IZ;FY+BLTmY z9%Fj~-wUg@I{uWgS|6LkA}@AQcz|A!1i`&Y>GpUjsM>7I&h5S7kpckknI17twt^ye zz*9c#h;KA!Eepju?C{`pK={rqOl6!}(@-3?8MJEq%vL8Y} zFrZp;>M>z=?u7ZsW6toyrjg6`vPh9+=r^-@j;#4GwX|+^dd5{h4fuuGX}Hu)IT6OB z)TPcYq(bdzZMEFdBLiCv*PabHUOVWqwj&F~j<3o7ql%9=B-5H;1%~EGb|ZXbdDU;} zrd*6QB>oXRka}gj-@g0HT`W6%FW|0*%U6G_FMTP)6(LsV zpxgck)A+$O9Kw&er#{LaMRe8+QwIZlAi69geV!_)6zFZNMo}k6Q}K+7uN?#>SI;$5 zdpl6U^~$y^=$m(A=f+FnPir2-@ng>J5Mk+8UEtTEs|4~3MXig95`=7*JR7IV6S(8a zEXhStcv%D9Hw{W4H7qCq^gFL7f0PyS)FnDY&0imxIB>X&zZx2C7(b$inAwn)LW8M; zNu6K)3+{;{{mcXDm4X+Z;e8CoVW_Wo6_DwkO+ht}W?gxlkUKW-@S)Z%WGi5L8Uh)j zj+0P?ah>2>%d+6tLH2sz;I1ka{JhvdOekY#E=hP*?(Iz|V4f0I=#Q9gS`Il!4KJFy zPY4$;qX6sCDU$NPmVQ8pA3f zF8djZJkW<@I(Fgpk~z$jV*9K)xpPL)-#2oH$tql{*q;MJWo3`MTs(YAq}9~g_*2sy z<1JB0 z`Bh64B|t;(oxEDgzycGw0@^)xFvFw}r5=ky(!4DP?XayWV`9>pU(}L|>uParm&ax8 zLY?a^54&8~{{i6C!vo$gCw2dcGX^`S&gMExuVenP!^lyevrA@-=M$h=;^|Wt^cvi-4#Ktuz!K z5jInS&MR>Pdx}G0g~J2Uy+}#_;6OgAL2Ie8>_cQ6iO-QAOp^BtxZXTz_0;XJ*5>>; zI6+Cz$#$xzOWYTG2D%f`dxH%5%NjfFcD-;Qf%A9GzP=3Gv!2z4?Yh8##gP>2SxF6} zJv5^5QBTZ?Rfj|3aR!@g_^H@tDH%GQwv!qSg>yWU$7??u&<~ch7KkA3PSdK*7eodd} z)(p46f*TPgjjP+SD;CJkW8Vq5CY8s)tx!HLu2?}Sv~MFCthK2iP1!y`hD0(XNNO}> zKhsump~(V>_-BHN2zz0ZxJl!nDZOr0k33`MZU&B98|){QaKpL|+7LwR^!XE&3VjD1 zb)qyf=u`hH;_nFz*OpWJX!T0bd3dz0=0=*)yHl0D-tc)lp-)CY-}|mhAVJ4Y%r0fS zwzT1*)YH=J+Dg7MI|<^HA~t>j;S7gAUteNSp8;j$(i(d@u9ee%#HkK!BUe<^gEJm*#@Xi0Emlamm*QLNEM6-M-%KC_6-Sh_F!nIVXBn7?hEW0xuEE#O1H zi-8g&drq-1v9))2#$hmdeiv0q19&E9OW-w_ z$?_l5p7=bedi_wmuC^=d7(`JTBv+;+WFJ@O*Dxj{)tV`~OQ8&}<$82guLY1g_+&bU zHDc`|bTM&Fv4^8%n5=qYM86CRlG$6PY>nR90O0GC7+D%O=k62L+rvD@jJI)eH3xvA0aKiCrH@EM{1rb`Y8o* z7q3rdGm+2(rwS}KXSN=D&8Fo=VtpbI9VQU51@P~zbcD(4l1&!@|5$^jdkKIaLSlvDFK9>0i(<=FcO6m&6UPc#B(viL#Ie0 zxG`YUUgKs02LSjuNClnD9x(LfQ%CO`JL!7-6}!-oLr~!QO`* zhz~pFN2oA@oz3FoSe{!x9NBY^9E4JQ(*_Bv+XW0}(0X9(w5HhkYmS znp8^Txz7yfC}T+&lHpZ4iNdqNYgI4vFY~{CbKa=49xftRff!wBDXXNMgl^-UMUTE! zsHSo~!qIa^3D#*>gV4q)!KMn0++oyP^qs_MDLOD`uM%FaK{t(M~bGG_pyjhPCN)L zgqCoCgYtcQIZ28;y^8JE2}UOJ<-`HzWGhk}63u&ilVXs|u~X#3#=DSPP8X;-Z^^@% z6-~K$WgR-42i$DtR;T!ZdOn*c-<+a(UI*BPFlrMzOvZtIR4*Y-XkG$_8U_D2Kpz5} zlKS2o$ZI6P8yH?ZfUzC`H@gT802iuS2}%g-tLYjdAe4w9&a8@qKcZVl7>_aqKyo4?^z8E*27Xkr+wmAP*epah6S_|U z+xsD6ot{@;>gCR*nZUn%0v@Fbv}ooQW4_vOF2co1l9;VSpt=j9a#tGq+aJZY_8xHt zr+$Bxj`e8Qwg#%w{Jar{wrE@iZU`}>w46kOWCZ__r|Mes@@KP{C`W6<10DBTk0R0F zjDOU(QD*TdyU=&Oh3x^%R$#i_{afbRy?6zyD!ipdrHexYtrqfl+e>O^3L@qRtQGGL zcAUgEi;5}Lh-epEXa|56e$fknM%!E+{Zw{lYYZ&2!60Y)a#mVcd4_{!B!b1HR)=Ul z*$TESi%hYk+eZg@epHXPPm$Rqk1lj*67F`qIZk};+-RnLsV70Uyh{wfFuFsi-0XE_ zg=;9{EV?e7sI=%zwh#)^eA}uMhz({w-%QZ*IV@t}5A)$)2P|ON`0pvvnQ=L%Ad%+Q zHT(@*OKTl#Lu<5vweRjc;+jd4YK~{RoXQy}<5Wf+=^LD(tYj#1Bhc+oqeBv#``_W; zMXf6m`g$fcrb?vv zP5{y(ylu$G>rnwXO!y&ED{$GVvxqIpdT|dN1aiKofIz}@=3CfbeX`Af{1OS~;-gT} zQwguRfGHpjrA#w0BRb9~(UI?x2gt+&5HJu_7gY0oHo2I*+3Kp`H#P9rJ&noZyopm1 zW5pJcI#%HKFQlc?N)(phS1}z6-+DzTck@+|oq`mltB!tba1-3MNY+XuO0MEvpu>X9 zKH-Ew!S%KjD|Fr~+Xo8~0#Oc1Lv1ui^QeoOQ$^p(v2??TQ0L^C~GPECdssCI}nt>AS^jiJ@?c=k0;D;PHyo{5~TOJ<+(praXt?QYMsr0sQp zO0Th*uuBJyguXc1@jm7pQ8_fg==%Y!9H}!}!OW5{9N&Sha37U4bya$9JAAjL%pUki zgf?1Qn65X{@IDyb5Eu1tN>RG-eGc+%~z9>TraOb_l#&&LLT^R5sJb2CsEcml$@q9lX zRWC#;uExrcDpW7KVI!FV66U|**jy@MX9mU6$JW!Sdk#ix}Y0ekJHkM$IyPV~=3r6V+9v_f*PJ2P{ZI_klfP4!h}=X0`O$_H688hbQKkOk|41=UGhdat^y`(NNZT z?S@igV-m;xGSFXk>-XTP%RecBK5mN%+^T|2$e;1i0WFvj>? zRQ}tPZOb1|mUjaCPn_$S<689ruC?a}lw#EzQ0s!4eOQb*En?pRMPhBHADuDyi?HnO zEJ8Rb@sX8af6x3l4 zsi|Yeda_g-v()a5*;H|}8a14Jqb@0qU1hf|QJOYS4Ks4pQ_e&Q7FcgcT>#p=z^U1m zFf}gO8l_Zsr)pnb;b!0xe5|2Xzb|>@9P8CSfFIcz?sv7Dm|T&Dj&x6yNwaeihI@sZ zaCXIL4H9#sFTFc2WsE`hQiny9!;h=%c`RV_gOz339QJK}&~)fyo@<8*{Lad_LWhg1 z;zxF)r+cZX86Kd-j=^9cT<7kDR9{j8<|74Ua0dYcJHH6G<&Ge5+xhxY-#!YWcHm-a zKM7nP`n?U7NZy#JQ^X0Xb&}#A*i~go*|^8I7_!Y(Pjfk=sx5R6OZ=-n_quKS?Z^;I zGlUxNn`$wn#|k#Jhuo)C&&3?y?BT$TuTAP(b`VC)y|6CeDHOvvml2QYiqChVIW=gG z8KU{K_D_D^_a3f!(@6uGZB|Zf`tY82ahvcsw14YSX^O&zNAAj7Z(hUcU9xHh2)1*l zPPq@WKSsT=*;>jAuI_anoQ;JV2#nfmwlm~@kIK(lB--0SnXH~61eDW5Dn-`^N@Ug` z11bmdFUn-tFXISIgG{k+lSEsF`L_Lcr)%~CBESVjiZwFSvIdYO#Y83x&uf?OGXREo z%PAKbf89vM1{|frr-$2xafG}BiRBj>`1@nbD&G{xYv>);^D-gy^n!Hic={1DXj919sE}SYy+>hZI+y2 zek+x$h*~FgZI0^M9=VfccH9g%$BlzOZaoDlsBtOmnCH0L&sz$!CL*+p@9ueG$UDlQ zOc+#JW&w?N1L)okKu{MQPmEPDzs}{~=?=2bzY)WTAI@Gi0DaSQ{K$OuuS`Ke#nqu~ ztz(!!VG=^XWe_Oei3rdI8P^=UG-893L3WG~732z) zhPrO@dsj20D%}H~lKoj$IZ>%Hpb7o!JbXmRkL&==d~htx?c@oz`Hub0YhHZ15^<2a zhRnG^Dto32Z0Bbww-)-D19M5aM02D(DTcgA7PGhhwD7fAAe=Cx9=px*o4aO31runO zP6v_pqIp?vy&laWA$EaZurb2G`Dk9TbS75sJe#ld%PajNlYYvw4D$RB^do7N%oKlV z8@e(#V8m+n9_^HdRr_-*B!W_+js`T7HuGSifUV;Y3$cf1^8>u>KHFR+swyMZ*K>Sb znG-9^VxY6Q;oU*1zY-bt73g>w*b~~0&hW(2Kimna9dBkpnt1Yk+;`!Kv;DI{GBv}0 zqylAE7JZMyXA0lnV5@TfIe_ziT3apa6x-q}W+QKF(dHe&m>zh{%S6}gQVfGl)iV~y zm4m6Q=gRqA9P0SSsxY~r$AyLm`P+fB_0hJg-AeYzlXyf5H8pua&9k-tR#UI%<2oN? zFOi2&5li$YZZ?|C^8?=#TmnJ1AUwq;k>%`GC1zVUmkMP@yj)7qMIx^PTOO;-QFi*c z4@}ggU(D3u9V(}tmd^EE;2D>{p5wO*O*IwUCJ#9-@A9WzP58cw2bRQO&rRz;ZNMvk zBIc^eeu6qeCiXD(GB+ngn*GAPKV*MNxqj;X9{YMC2xeFMcBpAYJkM6qFMl10)%iMB zkDzfAx{8SQ1NvV7`z5_Q+ga|T$vHT->E>)&^nn0+@VTicg9!zA48QvlHPh;)>#DLQ zPTiJgtB*+}^n$$JgUkJ0DgXf2(IA>wYdaq2_Z!tlXJ}&j=OQAD2)>~>I7YfoQwaR9(3~7`&Rf=~KOO<1`5`PtX+AK+s}}0_eO&$BB~n<*;aL8qXEm zYya}p1pbZOCN8JgU}pHa7zTrAAZ}AjJQtYW2hcAS5^!eNFS2^w3y)~LK9+_;DLF5dk$bl6D;01 z($I90p!n5dC1Q+hqm^{*G+3=INBdJ3a(Lv-qml!R!yk;_B4nzwr!Lz?v{M_9zA_hJi>np@&rckJ7KW~&LKwn zx53*K&sAU#JFr?5Dr!?2^K&&G8&}!Zw~6yL1|VC``VBklNBP)RgklUe64DpGU~y`N z9+FM9rSRldBBclTAN=c50XvGQB~KCl3pTlRT6nqD(@IcV?=s63Ni&TYNPw{w z{EgTbmf^)Pf{LM@lR0sT28XFHf);$j%?fFATuN}zKn*Km^<^6lYbMX2goKJod*|dSY)h=#B8_zD-)~(HH^{HB zDg4hr^QnwbzzGZ*LwrdX?X`3k6Dy*(?(9E*giyRWt4BDQk3fyy5TG)1UMb*2bg1A_SvFUN^s!mfI|Ga8J+d#azM-AmPEluV57N z$L1J+pQSM)vmd$o)5qT$uaac|jFI;iwf85cvc0!;*Vyin;Zc7dS4EtnNl4_3HjY zOR#Q}hnFB5`G%0RI3m!)@gP7F(WE9$L8O$j!2Z2k_rV?}i7kKruDf~VWAP6J(Ao}T zPjHC{8cHaV_XDty=TR)Pd3u+H@BWd4{pQ}P23?qRn>ZQyUzu zdWI0Ewj!VAM!r_wjpZa(dq04mMSGOMt{Kb^mJ6<0!9OmgEO5|gZwxCc*~)41L1W+- z1MgBBh}M7Oauh=Mx_WLiEzX+AOExi{M+ zafE^pvXZytpa~l!dnI)g$vQy5xaa{Yq882i(Um6N1t@nLgN5kx=zU_?4uvt_L=f{Q zr1nPLpPTRGlt4sXo!&hE*y2)IN?kv~?@cCgAX+jS!5dI2yaJ{!4PbPqUrv+*ZBahr zXM#bb0QFi7^B23+%hnBfWw0D3g<;8}%YKBMvJSEa5xncc9y3LKPc_zOhI-S|9oa0j z0zJYzZ>vJ^wqpTZ!_SMlQsKQ8Kc?!d0}G@ZaJYw>w}xjkptk#7#<$ykF^F-;Vt;2; zhE>YCokkhKiU?MJs==~iM| zI&YfmJI<6@xjj!!S-$Bf3NBE;Oc#!X`M;==AataF%coF1|3bC;=5|;F0gpOSt0^)u%f+e z1#h4riO2(0AAv>GkxGj>doH4!_T1B;&bXxaP3@Ef;u0rVR)|R;L$vOg`5Q;knS$j6 zKSJEZ>GC;d0|%t$vdk?__*Klq^o7zHThMC)IFZFtaE8hw&^3~o;cq`(w-4nMqiV+J zYk=VuYD(+?LLNWoiNxgUy(ka)4Lxy6_{T`yBKJO?6_{<%vBdHslsYDq+Np0qOFWO< z9-V=U%!s*SQ^^t%f==0-eZa&KDsNn_<)22l$h?916vI;8$4W& zQdmoHlfQ(wyXjs_-TWQt=dHX%L!AJdn@~!BHiX$iR^e=D!x?OAyux7)yM*s#YDF9&FgY$N zcG-UT9yA<>(`h9RQG4EU%&ofyrm^M%y}Wr2Ly(MF+r=KSxbl<7MsOwJ29hV|~7g788-K6A5UpcYiG}79<2e9l~{M!QkFi;jc<``d*fegIodt zfHk}vh!B_N@p9fRgqlGyleNtvVjG)ZCZ6Zp4fl}9_sep3g2cbT?LMy=G^L97bKkx9 zBqyp5+M0H?Ajs+5zNgX*B%tg|BX0f3uV?|>Q@7{eghE)`Dp_Ogwbyz~!TtdT1`g7}zLt&g#;dVZc5q$b@!RL* zq=y&QYqaf%A?oBOKR@=gTNRV=Q6QXo*Ux-$+w#vQH~@8mgKmc5!! zx>BrZZ+BL;R>lAY2o?}bU_Jl<{2Q`&k_HISqNA{<-~Loezx7M*CdKSb5c?%v>;_G$ zHcP~lYA5X~b1G|=$6=ZVjgzj=8vsxZXwo)Zuj0GFk03@CgiBN6m9ur%=Sr#tD1a$? zwPtw(z*w2Jh+?kD_W?jz6gR=|gg2a>0PyG^&t^`-&H#`htbPb8zNt2J@Ux}Gz8Xo_ zY34!G9Fk+ibht#HqLJPx4`|lUwyj#!E$LPkCrK~-0e95H9Lsr{4L@zk!;|(^4h_cP zU9$5Gq5_dJfTz6Uzbgna02W)75Z-EEAPbUd{0i7p2(Rq7Jpj$B%j1+D(53kAgAhPtMwB~xCOH5e zi8E?Ef4<|0parTtznzEQw+|+LQP_8r8AS9Xd=E9Z5ahL zw4q(o%bd-9&eAzX>dux#0U?bw;7G1S#LykTMv@djSsrA3Z=3lS_l{w9j0Nc|6pFXa zZ-%EQtFtp~hb|lYKU~zY!H0loMZ$R?kh=!_RRNT+)yMJV&2f1;XPJkY)6%8@YYAe| zNmS_EAfa3n_4Td^Ih_*EWMSYDPPOVfC`4h(opq+7?dJK za~?9wKE_<)R2!fEd-^)4s@9>oPPAX%G2mCrrnn~LR^ZPQy;MV895V}m%g0Oi0mKg3QNn0{zn|qN7zb0vlS8fIKo5uc4rVKT- z_CXczOhvp=^uDMk^6f9E?;B4|{!g_1Kig1~xzorcBQ04=Vwj|+^GzT}VJOTejBOEC z67i6qsg8p;g^7Oi%u0d&QD0B^*@d$qD%95mU}zeZ6VJTs5k61igZ_s;D1QQfB&MxNX7Ce~^_lJ& zyinMJCnr%2_y`nFQ`4}(&fM&1e~+W&Mc>&6J}4y zJ!pu%f(n}Xi!0i+Khu-(U~6%Y4)sU6p=l{NN4M;yE3$rcDx}7kmLO`2uyhT_$@N|c zVh0-ZOm(H;=R<1j9&TV1VSsrLlI!%ypblOINDz={>{O5jD=rCrEz^&hU%vgE_P+ox CT?!)r literal 0 HcmV?d00001 diff --git a/examples/graphs/2d/graphsaudio/graphsaudio.pyproject b/examples/graphs/2d/graphsaudio/graphsaudio.pyproject new file mode 100644 index 00000000..eff79191 --- /dev/null +++ b/examples/graphs/2d/graphsaudio/graphsaudio.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "GraphsAudio/Main.qml", "GraphsAudio/qmldir"] +} diff --git a/examples/graphs/2d/graphsaudio/main.py b/examples/graphs/2d/graphsaudio/main.py new file mode 100644 index 00000000..239aee03 --- /dev/null +++ b/examples/graphs/2d/graphsaudio/main.py @@ -0,0 +1,80 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +from __future__ import annotations + +import sys +from pathlib import Path +from PySide6.QtCore import QObject, QPointF, Slot, Signal +from PySide6.QtMultimedia import QAudioFormat, QAudioSource, QMediaDevices +from PySide6.QtWidgets import QMessageBox +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtGui import QGuiApplication + + +SAMPLE_COUNT = 2000 +RESOLUTION = 4 + + +class Audio(QObject): + dataUpdated = Signal(list) + + def __init__(self, device): + super().__init__() + + format_audio = QAudioFormat() + format_audio.setSampleRate(8000) + format_audio.setChannelCount(1) + format_audio.setSampleFormat(QAudioFormat.UInt8) + + self.device_name = device.description() + + self._audio_input = QAudioSource(device, format_audio, self) + self._io_device = self._audio_input.start() + self._io_device.readyRead.connect(self._readyRead) + + self._buffer = [QPointF(x, 0) for x in range(SAMPLE_COUNT)] + + def closeEvent(self, event): + if self._audio_input is not None: + self._audio_input.stop() + event.accept() + + @Slot() + def _readyRead(self): + data = self._io_device.readAll() + available_samples = data.size() // RESOLUTION + start = 0 + if (available_samples < SAMPLE_COUNT): + start = SAMPLE_COUNT - available_samples + for s in range(start): + self._buffer[s].setY(self._buffer[s + available_samples].y()) + + data_index = 0 + for s in range(start, SAMPLE_COUNT): + value = (ord(data[data_index]) - 128) / 128 + self._buffer[s].setY(value) + data_index = data_index + RESOLUTION + + self.dataUpdated.emit(self._buffer) + + +if __name__ == '__main__': + app = QGuiApplication(sys.argv) + engine = QQmlApplicationEngine() + + input_devices = QMediaDevices.audioInputs() + if not input_devices: + QMessageBox.warning(None, "audio", "There is no audio input device available.") + sys.exit(-1) + + audio_bridge = Audio(input_devices[0]) + engine.rootContext().setContextProperty("audio_bridge", audio_bridge) + + device = input_devices[0] + device_name = device.description() + engine.rootContext().setContextProperty("device_name", device_name) + + engine.addImportPath(Path(__file__).parent) + engine.loadFromModule("GraphsAudio", "Main") + + sys.exit(app.exec()) diff --git a/examples/graphs/3d/widgetgraphgallery/highlightseries.py b/examples/graphs/3d/widgetgraphgallery/highlightseries.py index 27dad470..58a0d531 100644 --- a/examples/graphs/3d/widgetgraphgallery/highlightseries.py +++ b/examples/graphs/3d/widgetgraphgallery/highlightseries.py @@ -26,6 +26,7 @@ class HighlightSeries(QSurface3DSeries): self._position = {} self._topographicSeries = None self._minHeight = 0.0 + self._height_adjustment = 5.0 self.setDrawMode(QSurface3DSeries.DrawFlag.DrawSurface) self.setShading(QSurface3DSeries.Shading.Flat) self.setVisible(False) @@ -72,7 +73,7 @@ class HighlightSeries(QSurface3DSeries): srcRow = srcArray[i] for j in range(startX, endX): pos = srcRow.at(j).position() - pos.setY(pos.y() + 0.1) + pos.setY(pos.y() + self._height_adjustment) item = QSurfaceDataItem(QVector3D(pos)) newRow.append(item) dataArray.append(newRow) @@ -93,3 +94,8 @@ class HighlightSeries(QSurface3DSeries): self.setBaseGradient(gr) self.setColorStyle(QGraphsTheme.ColorStyle.RangeGradient) + + self.handle_zoom_change(ratio) + + def handle_zoom_change(self, zoom): + self._height_adjustment = (1.2 - zoom) * 10.0 diff --git a/examples/graphs/3d/widgetgraphgallery/scatterdatamodifier.py b/examples/graphs/3d/widgetgraphgallery/scatterdatamodifier.py index 946c9626..984bf9df 100644 --- a/examples/graphs/3d/widgetgraphgallery/scatterdatamodifier.py +++ b/examples/graphs/3d/widgetgraphgallery/scatterdatamodifier.py @@ -5,7 +5,7 @@ from __future__ import annotations from enum import Enum from math import sin, cos, degrees, sqrt -from PySide6.QtCore import QObject, Signal, Slot, Qt +from PySide6.QtCore import QObject, Signal, Slot, Qt, QRandomGenerator from PySide6.QtGui import QVector2D, QVector3D from PySide6.QtGraphs import (QAbstract3DSeries, QScatterDataItem, QScatterDataProxy, @@ -27,7 +27,7 @@ class InputState(Enum): class ScatterDataModifier(QObject): - backgroundEnabledChanged = Signal(bool) + backgroundVisibleChanged = Signal(bool) gridVisibleChanged = Signal(bool) shadowQualityChanged = Signal(int) @@ -42,11 +42,11 @@ class ScatterDataModifier(QObject): self._itemCount = LOWER_NUMBER_OF_ITEMS self._CURVE_DIVIDER = LOWER_CURVE_DIVIDER - self._graph.activeTheme().setTheme(QGraphsTheme.Theme.MixSeries) - self._graph.activeTheme().setColorScheme(QGraphsTheme.ColorScheme.Dark) self._graph.setShadowQuality(QtGraphs3D.ShadowQuality.SoftHigh) self._graph.setCameraPreset(QtGraphs3D.CameraPreset.Front) self._graph.setCameraZoomLevel(80.0) + self._graph.activeTheme().setTheme(QGraphsTheme.Theme.MixSeries) + self._graph.activeTheme().setColorScheme(QGraphsTheme.ColorScheme.Dark) self._proxy = QScatterDataProxy() self._series = QScatter3DSeries(self._proxy) @@ -99,7 +99,7 @@ class ScatterDataModifier(QObject): def changeTheme(self, theme): currentTheme = self._graph.activeTheme() currentTheme.setTheme(QGraphsTheme.Theme(theme)) - self.backgroundEnabledChanged.emit(currentTheme.isPlotAreaBackgroundVisible()) + self.backgroundVisibleChanged.emit(currentTheme.isPlotAreaBackgroundVisible()) self.gridVisibleChanged.emit(currentTheme.isGridVisible()) @Slot() @@ -114,45 +114,6 @@ class ScatterDataModifier(QObject): def shadowQualityUpdatedByVisual(self, sq): self.shadowQualityChanged.emit(sq.value) - @Slot(int) - def changeShadowQuality(self, quality): - sq = QtGraphs3D.ShadowQuality(quality) - self._graph.setShadowQuality(sq) - - @Slot(int) - def setPlotAreaBackgroundVisible(self, state): - enabled = state == Qt.CheckState.Checked - self._graph.activeTheme().setPlotAreaBackgroundVisible(enabled) - - @Slot(int) - def setGridVisible(self, state): - self._graph.activeTheme().setGridVisible(state == Qt.Checked.value) - - @Slot() - def toggleItemCount(self): - if self._itemCount == NUMBER_OF_ITEMS: - self._itemCount = LOWER_NUMBER_OF_ITEMS - self._CURVE_DIVIDER = LOWER_CURVE_DIVIDER - else: - self._itemCount = NUMBER_OF_ITEMS - self._CURVE_DIVIDER = CURVE_DIVIDER - - self._graph.seriesList()[0].dataProxy().resetArray([]) - self.addData() - - @Slot() - def toggleRanges(self): - if not self._autoAdjust: - self._graph.axisX().setAutoAdjustRange(True) - self._graph.axisZ().setAutoAdjustRange(True) - self._dragSpeedModifier = 1.5 - self._autoAdjust = True - else: - self._graph.axisX().setRange(-10.0, 10.0) - self._graph.axisZ().setRange(-10.0, 10.0) - self._dragSpeedModifier = float(15) - self._autoAdjust = False - @Slot(QtGraphs3D.ElementType) def handleElementSelected(self, type): if type == QtGraphs3D.ElementType.AxisXLabel: @@ -197,3 +158,69 @@ class ScatterDataModifier(QObject): # No need to use adjusted y move here distance = move.y() / self._dragSpeedModifier axis.setRange(axis.min() + distance, axis.max() + distance) + + @Slot(int) + def changeShadowQuality(self, quality): + sq = QtGraphs3D.ShadowQuality(quality) + self._graph.setShadowQuality(sq) + + @Slot(int) + def setBackgroundVisible(self, state): + enabled = state == Qt.CheckState.Checked + self._graph.activeTheme().setPlotAreaBackgroundVisible(enabled) + + @Slot(int) + def setGridVisible(self, state): + self._graph.activeTheme().setGridVisible(state == Qt.Checked.value) + + @Slot() + def toggleItemCount(self): + if self._itemCount == NUMBER_OF_ITEMS: + self._itemCount = LOWER_NUMBER_OF_ITEMS + self._CURVE_DIVIDER = LOWER_CURVE_DIVIDER + else: + self._itemCount = NUMBER_OF_ITEMS + self._CURVE_DIVIDER = CURVE_DIVIDER + + self._graph.seriesList()[0].dataProxy().resetArray([]) + self.addData() + + @Slot() + def toggleRanges(self): + if not self._autoAdjust: + self._graph.axisX().setAutoAdjustRange(True) + self._graph.axisZ().setAutoAdjustRange(True) + self._dragSpeedModifier = 1.5 + self._autoAdjust = True + else: + self._graph.axisX().setRange(-10.0, 10.0) + self._graph.axisZ().setRange(-10.0, 10.0) + self._dragSpeedModifier = float(15) + self._autoAdjust = False + + def adjust_minimum_range(self, range): + if self._itemCount == LOWER_NUMBER_OF_ITEMS: + range *= 1.45 + else: + range *= 4.95 + + self._graph.axisX().setMin(range) + self._graph.axisZ().setMin(range) + self._autoAdjust = False + + def adjust_maximum_range(self, range): + if self._itemCount == LOWER_NUMBER_OF_ITEMS: + range *= 1.45 + else: + range *= 4.95 + + self._graph.axisX().setMax(range) + self._graph.axisZ().setMax(range) + self._autoAdjust = False + + def rand_vector() -> QVector3D: + generator = QRandomGenerator.global_() + x = float(generator.bounded(100)) / 2.0 - float(generator.bounded(100)) / 2.0 + y = float(generator.bounded(100)) / 100.0 - float(generator.bounded(100)) / 100.0 + z = float(generator.bounded(100)) / 2.0 - float(generator.bounded(100)) / 2.0 + return QVector3D(x, y, z) diff --git a/examples/graphs/3d/widgetgraphgallery/scattergraph.py b/examples/graphs/3d/widgetgraphgallery/scattergraph.py index 1b5c507a..050ce285 100644 --- a/examples/graphs/3d/widgetgraphgallery/scattergraph.py +++ b/examples/graphs/3d/widgetgraphgallery/scattergraph.py @@ -5,7 +5,7 @@ from __future__ import annotations from PySide6.QtCore import QObject, QSize, Qt from PySide6.QtWidgets import (QCheckBox, QComboBox, QCommandLinkButton, QLabel, QHBoxLayout, QSizePolicy, - QVBoxLayout, QWidget, ) + QVBoxLayout, QWidget, QSlider) from PySide6.QtQuickWidgets import QQuickWidget from PySide6.QtGraphs import QAbstract3DSeries from PySide6.QtGraphsWidgets import Q3DScatterWidgetItem @@ -42,10 +42,15 @@ class ScatterGraph(QObject): itemCountButton.setDescription("Switch between 900 and 10000 data points") itemCountButton.setIconSize(QSize(0, 0)) - rangeButton = QCommandLinkButton(self._scatterWidget) - rangeButton.setText("Toggle axis ranges") - rangeButton.setDescription("Switch between automatic axis ranges and preset ranges") - rangeButton.setIconSize(QSize(0, 0)) + range_min_slider = QSlider(Qt.Horizontal, self._scatterWidget) + range_min_slider.setMinimum(-10) + range_min_slider.setMaximum(1) + range_min_slider.setValue(-10) + + range_max_slider = QSlider(Qt.Horizontal, self._scatterWidget) + range_max_slider.setMinimum(1) + range_max_slider.setMaximum(10) + range_max_slider.setValue(10) backgroundCheckBox = QCheckBox(self._scatterWidget) backgroundCheckBox.setText("Show graph background") @@ -89,7 +94,8 @@ class ScatterGraph(QObject): vLayout.addWidget(cameraButton) vLayout.addWidget(itemCountButton) - vLayout.addWidget(rangeButton) + vLayout.addWidget(range_min_slider) + vLayout.addWidget(range_max_slider) vLayout.addWidget(backgroundCheckBox) vLayout.addWidget(gridCheckBox) vLayout.addWidget(smoothCheckBox) @@ -104,13 +110,14 @@ class ScatterGraph(QObject): cameraButton.clicked.connect(modifier.changePresetCamera) itemCountButton.clicked.connect(modifier.toggleItemCount) - rangeButton.clicked.connect(modifier.toggleRanges) + range_min_slider.valueChanged.connect(modifier.adjust_minimum_range) + range_max_slider.valueChanged.connect(modifier.adjust_maximum_range) - backgroundCheckBox.checkStateChanged.connect(modifier.setPlotAreaBackgroundVisible) + backgroundCheckBox.checkStateChanged.connect(modifier.setBackgroundVisible) gridCheckBox.checkStateChanged.connect(modifier.setGridVisible) smoothCheckBox.checkStateChanged.connect(modifier.setSmoothDots) - modifier.backgroundEnabledChanged.connect(backgroundCheckBox.setChecked) + modifier.backgroundVisibleChanged.connect(backgroundCheckBox.setChecked) modifier.gridVisibleChanged.connect(gridCheckBox.setChecked) itemStyleList.currentIndexChanged.connect(modifier.changeStyle) diff --git a/examples/gui/analogclock/main.py b/examples/gui/analogclock/main.py index 28ce3454..10ed66e3 100644 --- a/examples/gui/analogclock/main.py +++ b/examples/gui/analogclock/main.py @@ -5,7 +5,8 @@ from __future__ import annotations import sys from PySide6.QtCore import QPoint, QTimer, QTime, Qt -from PySide6.QtGui import QGuiApplication, QPainter, QPalette, QPolygon, QRasterWindow +from PySide6.QtGui import (QGuiApplication, QPainter, QPainterStateGuard, + QPalette, QPolygon, QRasterWindow) """Simplified PySide6 port of the gui/analogclock example from Qt v6.x""" @@ -54,10 +55,9 @@ class AnalogClockWindow(QRasterWindow): painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(self._hour_color) - painter.save() - painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0))) - painter.drawConvexPolygon(self._hour_hand) - painter.restore() + with QPainterStateGuard(painter): + painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0))) + painter.drawConvexPolygon(self._hour_hand) for _ in range(0, 12): painter.drawRect(73, -3, 16, 6) @@ -65,19 +65,17 @@ class AnalogClockWindow(QRasterWindow): painter.setBrush(self._minute_color) - painter.save() - painter.rotate(6.0 * time.minute()) - painter.drawConvexPolygon(self._minute_hand) - painter.restore() + with QPainterStateGuard(painter): + painter.rotate(6.0 * time.minute()) + painter.drawConvexPolygon(self._minute_hand) painter.setBrush(self._seconds_color) - painter.save() - painter.rotate(6.0 * time.second()) - painter.drawConvexPolygon(self._seconds_hand) - painter.drawEllipse(-3, -3, 6, 6) - painter.drawEllipse(-5, -68, 10, 10) - painter.restore() + with QPainterStateGuard(painter): + painter.rotate(6.0 * time.second()) + painter.drawConvexPolygon(self._seconds_hand) + painter.drawEllipse(-3, -3, 6, 6) + painter.drawEllipse(-5, -68, 10, 10) painter.setPen(self._minute_color) diff --git a/examples/multimedia/camera/camera.py b/examples/multimedia/camera/camera.py index ecc8f701..90a8fed4 100644 --- a/examples/multimedia/camera/camera.py +++ b/examples/multimedia/camera/camera.py @@ -296,7 +296,7 @@ class Camera(QMainWindow): @Slot(QAction) def updateCameraDevice(self, action): - self.setCamera(QCameraDevice(action)) + self.setCamera(QCameraDevice(action.data())) @Slot() def displayViewfinder(self): diff --git a/examples/webenginequick/nanobrowser/ApplicationRoot.qml b/examples/webenginequick/nanobrowser/ApplicationRoot.qml index 55c41440..f3624980 100644 --- a/examples/webenginequick/nanobrowser/ApplicationRoot.qml +++ b/examples/webenginequick/nanobrowser/ApplicationRoot.qml @@ -7,13 +7,16 @@ import QtWebEngine QtObject { id: root - property QtObject defaultProfile: WebEngineProfile { + property QtObject defaultProfilePrototype : WebEngineProfilePrototype { storageName: "Profile" - offTheRecord: false + Component.onCompleted: { + let fullVersionList = defaultProfilePrototype.instance().clientHints.fullVersionList; + fullVersionList["QuickNanoBrowser"] = "1.0"; + defaultProfilePrototype.instance().clientHints.fullVersionList = fullVersionList; + } } - property QtObject otrProfile: WebEngineProfile { - offTheRecord: true + property QtObject otrPrototype : WebEngineProfilePrototype { } property Component browserWindowComponent: BrowserWindow { @@ -34,7 +37,7 @@ QtObject { return newDialog; } function load(url) { - var browserWindow = createWindow(defaultProfile); + var browserWindow = createWindow(defaultProfilePrototype.instance()); browserWindow.currentWebView.url = url; } } diff --git a/examples/webenginequick/nanobrowser/BrowserWindow.qml b/examples/webenginequick/nanobrowser/BrowserWindow.qml index a517c5a5..365d77d2 100644 --- a/examples/webenginequick/nanobrowser/BrowserWindow.qml +++ b/examples/webenginequick/nanobrowser/BrowserWindow.qml @@ -4,7 +4,8 @@ import QtCore import QtQml import QtQuick -import QtQuick.Controls +import QtQuick.Controls.Fusion +import QtQuick.Dialogs import QtQuick.Layouts import QtQuick.Window import QtWebEngine @@ -44,6 +45,7 @@ ApplicationWindow { property alias webRTCPublicInterfacesOnly : webRTCPublicInterfacesOnly.checked property alias devToolsEnabled: devToolsEnabled.checked property alias pdfViewerEnabled: pdfViewerEnabled.checked + property int imageAnimationPolicy: WebEngineSettings.ImageAnimationPolicy.Allow } Action { @@ -70,7 +72,7 @@ ApplicationWindow { Action { shortcut: StandardKey.AddTab onTriggered: { - tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfile); + tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfilePrototype.instance()); addressBar.forceActiveFocus(); addressBar.selectAll(); } @@ -317,10 +319,10 @@ ApplicationWindow { id: offTheRecordEnabled text: "Off The Record" checkable: true - checked: currentWebView && currentWebView.profile === otrProfile + checked: currentWebView && currentWebView.profile === otrPrototype.instance() onToggled: function(checked) { if (currentWebView) { - currentWebView.profile = checked ? otrProfile : defaultProfile; + currentWebView.profile = checked ? otrPrototype.instance() : defaultProfilePrototype.instance(); } } } @@ -362,10 +364,49 @@ ApplicationWindow { } MenuItem { id: pdfViewerEnabled - text: "PDF viewer enabled" + text: "PDF Viewer Enabled" checkable: true checked: WebEngine.settings.pdfViewerEnabled } + + Menu { + id: imageAnimationPolicy + title: "Image Animation Policy" + + MenuItem { + id: disableImageAnimation + text: "Disable All Image Animation" + checkable: true + autoExclusive: true + checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.ImageAnimationPolicy.Disallow + onTriggered: { + appSettings.imageAnimationPolicy = WebEngineSettings.ImageAnimationPolicy.Disallow + } + } + + MenuItem { + id: allowImageAnimation + text: "Allow All Animated Images" + checkable: true + autoExclusive: true + checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.ImageAnimationPolicy.Allow + onTriggered : { + appSettings.imageAnimationPolicy = WebEngineSettings.ImageAnimationPolicy.Allow + } + } + + MenuItem { + id: animateImageOnce + text: "Animate Image Once" + checkable: true + autoExclusive: true + checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.ImageAnimationPolicy.AnimateOnce + onTriggered : { + appSettings.imageAnimationPolicy = WebEngineSettings.ImageAnimationPolicy.AnimateOnce + } + } + } + } } } @@ -455,7 +496,7 @@ ApplicationWindow { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - Component.onCompleted: createTab(defaultProfile) + Component.onCompleted: createTab(defaultProfilePrototype.instance()) function createTab(profile, focusOnNewTab = true, url = undefined) { var webview = tabComponent.createObject(tabLayout, {profile: profile}); @@ -471,7 +512,6 @@ ApplicationWindow { } function removeView(index) { - tabBar.removeItem(index); if (tabBar.count > 1) { tabBar.removeItem(tabBar.itemAt(index)); tabLayout.children[index].destroy(); @@ -521,8 +561,15 @@ ApplicationWindow { settings.touchIconsEnabled: appSettings.touchIconsEnabled settings.webRTCPublicInterfacesOnly: appSettings.webRTCPublicInterfacesOnly settings.pdfViewerEnabled: appSettings.pdfViewerEnabled + settings.imageAnimationPolicy: appSettings.imageAnimationPolicy + settings.screenCaptureEnabled: true onCertificateError: function(error) { + if (!error.isMainFrame) { + error.rejectCertificate(); + return; + } + error.defer(); sslDialog.enqueue(error); } @@ -565,6 +612,11 @@ ApplicationWindow { request.accept(); } + onDesktopMediaRequested: function(request) { + // select the primary screen + request.selectScreen(request.screensModel.index(0, 0)); + } + onRenderProcessTerminated: function(terminationStatus, exitCode) { var status = ""; switch (terminationStatus) { @@ -603,10 +655,12 @@ ApplicationWindow { findBar.reset(); } - onFeaturePermissionRequested: function(securityOrigin, feature) { - featurePermissionDialog.securityOrigin = securityOrigin; - featurePermissionDialog.feature = feature; - featurePermissionDialog.visible = true; + onPermissionRequested: function(permission) { + permissionDialog.permission = permission; + permissionDialog.visible = true; + } + onWebAuthUxRequested: function(request) { + webAuthDialog.init(request); } Timer { @@ -688,7 +742,7 @@ ApplicationWindow { } } Dialog { - id: featurePermissionDialog + id: permissionDialog anchors.centerIn: parent width: Math.min(browserWindow.width, browserWindow.height) / 3 * 2 contentWidth: mainTextForPermissionDialog.width @@ -696,53 +750,59 @@ ApplicationWindow { standardButtons: Dialog.No | Dialog.Yes title: "Permission Request" - property var feature; - property url securityOrigin; + property var permission; contentItem: Item { Label { id: mainTextForPermissionDialog - text: featurePermissionDialog.questionForFeature() } } - onAccepted: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, true) - onRejected: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, false) + onAccepted: permission.grant() + onRejected: permission.deny() onVisibleChanged: { - if (visible) + if (visible) { + mainTextForPermissionDialog.text = questionForPermissionType(); width = contentWidth + 20; + } } - function questionForFeature() { - var question = "Allow " + securityOrigin + " to " + function questionForPermissionType() { + var question = "Allow " + permission.origin + " to " - switch (feature) { - case WebEngineView.Geolocation: + switch (permission.permissionType) { + case WebEnginePermission.PermissionType.Geolocation: question += "access your location information?"; break; - case WebEngineView.MediaAudioCapture: + case WebEnginePermission.PermissionType.MediaAudioCapture: question += "access your microphone?"; break; - case WebEngineView.MediaVideoCapture: + case WebEnginePermission.PermissionType.MediaVideoCapture: question += "access your webcam?"; break; - case WebEngineView.MediaVideoCapture: + case WebEnginePermission.PermissionType.MediaAudioVideoCapture: question += "access your microphone and webcam?"; break; - case WebEngineView.MouseLock: + case WebEnginePermission.PermissionType.MouseLock: question += "lock your mouse cursor?"; break; - case WebEngineView.DesktopVideoCapture: + case WebEnginePermission.PermissionType.DesktopVideoCapture: question += "capture video of your desktop?"; break; - case WebEngineView.DesktopAudioVideoCapture: + case WebEnginePermission.PermissionType.DesktopAudioVideoCapture: question += "capture audio and video of your desktop?"; break; - case WebEngineView.Notifications: + case WebEnginePermission.PermissionType.Notifications: question += "show notification on your desktop?"; break; + case WebEnginePermission.PermissionType.ClipboardReadWrite: + question += "read from and write to your clipboard?"; + break; + case WebEnginePermission.PermissionType.LocalFontsAccess: + question += "access the fonts stored on your machine?"; + break; default: - question += "access unknown or unsupported feature [" + feature + "] ?"; + question += "access unknown or unsupported permission type [" + permission.permissionType + "] ?"; break; } @@ -760,10 +820,34 @@ ApplicationWindow { anchors.fill: parent } + WebAuthDialog { + id: webAuthDialog + visible: false + } + + MessageDialog { + id: downloadAcceptDialog + property var downloadRequest: downloadView.pendingDownloadRequest + title: "Download requested" + text: downloadRequest ? downloadRequest.suggestedFileName : "" + buttons: Dialog.No | Dialog.Yes + onAccepted: { + downloadView.visible = true; + downloadView.append(downloadRequest); + downloadRequest.accept(); + } + onRejected: { + downloadRequest.cancel(); + } + onButtonClicked: { + visible = false; + } + visible: false + } + function onDownloadRequested(download) { - downloadView.visible = true; - downloadView.append(download); - download.accept(); + downloadView.pendingDownloadRequest = download; + downloadAcceptDialog.visible = true; } FindBar { diff --git a/examples/webenginequick/nanobrowser/DownloadView.qml b/examples/webenginequick/nanobrowser/DownloadView.qml index e16647cd..b116ab86 100644 --- a/examples/webenginequick/nanobrowser/DownloadView.qml +++ b/examples/webenginequick/nanobrowser/DownloadView.qml @@ -2,13 +2,14 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import QtQuick.Controls +import QtQuick.Controls.Fusion import QtWebEngine import QtQuick.Layouts Rectangle { id: downloadView color: "lightgray" + property var pendingDownloadRequest: null ListModel { id: downloadModel diff --git a/examples/webenginequick/nanobrowser/FindBar.qml b/examples/webenginequick/nanobrowser/FindBar.qml index 4d130a22..409d8dcf 100644 --- a/examples/webenginequick/nanobrowser/FindBar.qml +++ b/examples/webenginequick/nanobrowser/FindBar.qml @@ -2,7 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import QtQuick.Controls +import QtQuick.Controls.Fusion import QtQuick.Layouts Rectangle { @@ -51,6 +51,7 @@ Rectangle { TextField { id: findTextField anchors.fill: parent + color: "black" background: Rectangle { color: "transparent" } @@ -64,6 +65,7 @@ Rectangle { Label { text: activeMatch + "/" + numberOfMatches visible: findTextField.text != "" + color: "black" } Rectangle { @@ -79,17 +81,29 @@ Rectangle { text: "<" enabled: numberOfMatches > 0 onClicked: root.findPrevious() + contentItem: Text { + color: "black" + text: parent.text + } } ToolButton { text: ">" enabled: numberOfMatches > 0 onClicked: root.findNext() + contentItem: Text { + color: "black" + text: parent.text + } } ToolButton { text: "x" onClicked: root.visible = false + contentItem: Text { + color: "black" + text: parent.text + } } } } diff --git a/examples/webenginequick/nanobrowser/quicknanobrowser.py b/examples/webenginequick/nanobrowser/quicknanobrowser.py index e5c667c5..f68cc2e3 100644 --- a/examples/webenginequick/nanobrowser/quicknanobrowser.py +++ b/examples/webenginequick/nanobrowser/quicknanobrowser.py @@ -54,7 +54,7 @@ if __name__ == '__main__': nargs='?', type=str) options = argument_parser.parse_args() - url = url_from_user_input(options.url) if options.url else QUrl("https://www.qt.io") + url = url_from_user_input(options.url) if options.url else QUrl("chrome://qt") app_args = sys.argv if options.single_process: diff --git a/examples/webenginewidgets/simplebrowser/browser.py b/examples/webenginewidgets/simplebrowser/browser.py index 1bf458e7..29e45208 100644 --- a/examples/webenginewidgets/simplebrowser/browser.py +++ b/examples/webenginewidgets/simplebrowser/browser.py @@ -22,9 +22,6 @@ class Browser(QObject): # remaining window self._download_manager_widget.setAttribute(Qt.WidgetAttribute.WA_QuitOnClose, False) - dp = QWebEngineProfile.defaultProfile() - dp.downloadRequested.connect(self._download_manager_widget.download_requested) - def create_hidden_window(self, offTheRecord=False): if not offTheRecord and not self._profile: name = "simplebrowser." + qWebEngineChromiumVersion() @@ -34,11 +31,15 @@ class Browser(QObject): s.setAttribute(QWebEngineSettings.WebAttribute.DnsPrefetchEnabled, True) s.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) s.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, False) + s.setAttribute(QWebEngineSettings.ScreenCaptureEnabled, True) self._profile.downloadRequested.connect( self._download_manager_widget.download_requested) profile = QWebEngineProfile.defaultProfile() if offTheRecord else self._profile main_window = BrowserWindow(self, profile, False) + profile.setPersistentPermissionsPolicy( + QWebEngineProfile.PersistentPermissionsPolicy.AskEveryTime) + self._windows.append(main_window) main_window.about_to_close.connect(self._remove_window) return main_window diff --git a/examples/webenginewidgets/simplebrowser/main.py b/examples/webenginewidgets/simplebrowser/main.py index 2602c5db..3d42974a 100644 --- a/examples/webenginewidgets/simplebrowser/main.py +++ b/examples/webenginewidgets/simplebrowser/main.py @@ -16,6 +16,7 @@ from browser import Browser import data.rc_simplebrowser # noqa: F401 + if __name__ == "__main__": parser = ArgumentParser(description="Qt Widgets Web Browser", formatter_class=RawTextHelpFormatter) @@ -34,13 +35,15 @@ if __name__ == "__main__": QLoggingCategory.setFilterRules("qt.webenginecontext.debug=true") s = QWebEngineProfile.defaultProfile().settings() - s.setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True) - s.setAttribute(QWebEngineSettings.WebAttribute.DnsPrefetchEnabled, True) + s.setAttribute(QWebEngineSettings.PluginsEnabled, True) + s.setAttribute(QWebEngineSettings.DnsPrefetchEnabled, True) + s.setAttribute(QWebEngineSettings.ScreenCaptureEnabled, True) browser = Browser() window = browser.create_hidden_window() - url = QUrl.fromUserInput(args.url) if args.url else QUrl("https://www.qt.io") + url = QUrl.fromUserInput(args.url) if args.url else QUrl("chrome://qt") window.tab_widget().set_url(url) window.show() + sys.exit(app.exec()) diff --git a/examples/webenginewidgets/simplebrowser/ui_webauthdialog.py b/examples/webenginewidgets/simplebrowser/ui_webauthdialog.py new file mode 100644 index 00000000..eb54ba64 --- /dev/null +++ b/examples/webenginewidgets/simplebrowser/ui_webauthdialog.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'webauthdialog.ui' +## +## Created by: Qt User Interface Compiler version 6.8.1 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, + QGroupBox, QLabel, QLayout, QLineEdit, + QSizePolicy, QVBoxLayout, QWidget) + +class Ui_WebAuthDialog(object): + def setupUi(self, WebAuthDialog): + if not WebAuthDialog.objectName(): + WebAuthDialog.setObjectName(u"WebAuthDialog") + WebAuthDialog.resize(563, 397) + self.buttonBox = QDialogButtonBox(WebAuthDialog) + self.buttonBox.setObjectName(u"buttonBox") + self.buttonBox.setGeometry(QRect(20, 320, 471, 32)) + self.buttonBox.setOrientation(Qt.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok|QDialogButtonBox.Retry) + self.m_headingLabel = QLabel(WebAuthDialog) + self.m_headingLabel.setObjectName(u"m_headingLabel") + self.m_headingLabel.setGeometry(QRect(30, 20, 321, 16)) + self.m_headingLabel.setWordWrap(False) + self.m_description = QLabel(WebAuthDialog) + self.m_description.setObjectName(u"m_description") + self.m_description.setGeometry(QRect(30, 60, 491, 31)) + self.m_description.setWordWrap(False) + self.layoutWidget = QWidget(WebAuthDialog) + self.layoutWidget.setObjectName(u"layoutWidget") + self.layoutWidget.setGeometry(QRect(20, 100, 471, 171)) + self.m_mainVerticalLayout = QVBoxLayout(self.layoutWidget) + self.m_mainVerticalLayout.setObjectName(u"m_mainVerticalLayout") + self.m_mainVerticalLayout.setSizeConstraint(QLayout.SetDefaultConstraint) + self.m_mainVerticalLayout.setContentsMargins(0, 0, 0, 0) + self.m_pinGroupBox = QGroupBox(self.layoutWidget) + self.m_pinGroupBox.setObjectName(u"m_pinGroupBox") + self.m_pinGroupBox.setFlat(True) + self.m_pinLabel = QLabel(self.m_pinGroupBox) + self.m_pinLabel.setObjectName(u"m_pinLabel") + self.m_pinLabel.setGeometry(QRect(10, 20, 58, 16)) + self.m_pinLineEdit = QLineEdit(self.m_pinGroupBox) + self.m_pinLineEdit.setObjectName(u"m_pinLineEdit") + self.m_pinLineEdit.setGeometry(QRect(90, 20, 113, 21)) + self.m_confirmPinLabel = QLabel(self.m_pinGroupBox) + self.m_confirmPinLabel.setObjectName(u"m_confirmPinLabel") + self.m_confirmPinLabel.setGeometry(QRect(10, 50, 81, 16)) + self.m_confirmPinLineEdit = QLineEdit(self.m_pinGroupBox) + self.m_confirmPinLineEdit.setObjectName(u"m_confirmPinLineEdit") + self.m_confirmPinLineEdit.setGeometry(QRect(90, 50, 113, 21)) + self.m_pinEntryErrorLabel = QLabel(self.m_pinGroupBox) + self.m_pinEntryErrorLabel.setObjectName(u"m_pinEntryErrorLabel") + self.m_pinEntryErrorLabel.setGeometry(QRect(10, 80, 441, 16)) + + self.m_mainVerticalLayout.addWidget(self.m_pinGroupBox) + + + self.retranslateUi(WebAuthDialog) + + QMetaObject.connectSlotsByName(WebAuthDialog) + # setupUi + + def retranslateUi(self, WebAuthDialog): + WebAuthDialog.setWindowTitle(QCoreApplication.translate("WebAuthDialog", u"Dialog", None)) + self.m_headingLabel.setText(QCoreApplication.translate("WebAuthDialog", u"Heading", None)) + self.m_description.setText(QCoreApplication.translate("WebAuthDialog", u"Description", None)) + self.m_pinGroupBox.setTitle("") + self.m_pinLabel.setText(QCoreApplication.translate("WebAuthDialog", u"PIN", None)) + self.m_confirmPinLabel.setText(QCoreApplication.translate("WebAuthDialog", u"Confirm PIN", None)) + self.m_pinEntryErrorLabel.setText(QCoreApplication.translate("WebAuthDialog", u"TextLabel", None)) + # retranslateUi + diff --git a/examples/webenginewidgets/simplebrowser/webauthdialog.py b/examples/webenginewidgets/simplebrowser/webauthdialog.py new file mode 100644 index 00000000..162c595d --- /dev/null +++ b/examples/webenginewidgets/simplebrowser/webauthdialog.py @@ -0,0 +1,243 @@ +# Converted from webauthdialog.cpp + +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from ui_webauthdialog import Ui_WebAuthDialog + +from PySide6.QtWidgets import (QDialog, QVBoxLayout, QButtonGroup, + QScrollArea, QWidget, QDialogButtonBox, + QSizePolicy, QRadioButton) +from PySide6.QtCore import Qt +from PySide6.QtWebEngineCore import QWebEngineWebAuthUxRequest + + +class WebAuthDialog(QDialog): + + def __init__(self, request, parent=None): + super().__init__(parent) + + self.uxRequest = request + self.uiWebAuthDialog = Ui_WebAuthDialog() + self.uiWebAuthDialog.setupUi(self) + + self.button_group = QButtonGroup(self) + self.button_group.setExclusive(True) + + self.scroll_area = QScrollArea(self) + self.select_account_widget = QWidget(self) + self.scroll_area.setWidget(self.select_account_widget) + self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.select_account_widget.resize(400, 150) + + self.select_account_layout = QVBoxLayout(self.select_account_widget) + self.uiWebAuthDialog.m_mainVerticalLayout.addWidget(self.scroll_area) + self.select_account_layout.setAlignment(Qt.AlignTop) + + self.update_display() + + self.uiWebAuthDialog.buttonBox.rejected.connect(self.onCancelRequest) + self.uiWebAuthDialog.buttonBox.accepted.connect(self.onAcceptRequest) + + button = self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Retry) + button.clicked.connect(self.onRetry) + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) + + def __del__(self): + for button in self.button_group.buttons(): + button.deleteLater() + + if self.button_group: + self.button_group.deleteLater() + self.button_group = None + + if self.uiWebAuthDialog: + del self.uiWebAuthDialog + self.uiWebAuthDialog = None + + if self.scroll_area: + self.scroll_area.deleteLater() + self.scroll_area = None + + def update_display(self): + state = self.uxRequest.state() + if state == QWebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: + self.setupSelectAccountUI() + elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: + self.setupCollectPinUI() + elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.FinishTokenCollection: + self.setupFinishCollectTokenUI() + elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.RequestFailed: + self.setupErrorUI() + + self.adjustSize() + + def setupSelectAccountUI(self): + self.uiWebAuthDialog.m_headingLabel.setText(self.tr("Choose a Passkey")) + self.uiWebAuthDialog.m_description.setText(self.tr("Which passkey do you want to use for ") + + self.uxRequest.relyingPartyId() + + self.tr("? ")) + self.uiWebAuthDialog.m_pinGroupBox.setVisible(False) + self.uiWebAuthDialog.m_mainVerticalLayout.removeWidget(self.uiWebAuthDialog.m_pinGroupBox) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Retry).setVisible(False) + + self.clearSelectAccountButtons() + self.scroll_area.setVisible(True) + self.select_account_widget.resize(self.width(), self.height()) + userNames = self.uxRequest.userNames() + # Create radio buttons for each name + for name in userNames: + radioButton = QRadioButton(name) + self.select_account_layout.addWidget(radioButton) + self.button_group.addButton(radioButton) + + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Ok).setText(self.tr("Ok")) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Ok).setVisible(True) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Cancel).setVisible(True) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Retry).setVisible(False) + + def setupFinishCollectTokenUI(self): + + self.clearSelectAccountButtons() + self.uiWebAuthDialog.m_headingLabel.setText(self.tr("Use your security key with") + + self.uxRequest.relyingPartyId()) + self.uiWebAuthDialog.m_description.setText( + self.tr("Touch your security key again to complete the request.")) + self.uiWebAuthDialog.m_pinGroupBox.setVisible(False) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Ok).setVisible(False) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Retry).setVisible(False) + self.scroll_area.setVisible(False) + + def setupCollectPinUI(self): + + self.clearSelectAccountButtons() + self.uiWebAuthDialog.m_mainVerticalLayout.addWidget(self.uiWebAuthDialog.m_pinGroupBox) + self.uiWebAuthDialog.m_pinGroupBox.setVisible(True) + self.uiWebAuthDialog.m_confirmPinLabel.setVisible(False) + self.uiWebAuthDialog.m_confirmPinLineEdit.setVisible(False) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Ok).setText(self.tr("Next")) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Ok).setVisible(True) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Cancel).setVisible(True) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Retry).setVisible(False) + self.scroll_area.setVisible(False) + + pinRequestInfo = self.uxRequest.pinRequest() + + if pinRequestInfo.reason == QWebEngineWebAuthUxRequest.PinEntryReason.Challenge: + self.uiWebAuthDialog.m_headingLabel.setText(self.tr("PIN Required")) + self.uiWebAuthDialog.m_description.setText( + self.tr("Enter the PIN for your security key")) + self.uiWebAuthDialog.m_confirmPinLabel.setVisible(False) + self.uiWebAuthDialog.m_confirmPinLineEdit.setVisible(False) + else: + if pinRequestInfo.reason == QWebEngineWebAuthUxRequest.PinEntryReason.Set: + self.uiWebAuthDialog.m_headingLabel.setText(self.tr("New PIN Required")) + self.uiWebAuthDialog.m_description.setText( + self.tr("Set new PIN for your security key")) + else: + self.uiWebAuthDialog.m_headingLabel.setText(self.tr("Change PIN Required")) + self.uiWebAuthDialog.m_description.setText( + self.tr("Change PIN for your security key")) + + self.uiWebAuthDialog.m_confirmPinLabel.setVisible(True) + self.uiWebAuthDialog.m_confirmPinLineEdit.setVisible(True) + + errorDetails = "" + + if pinRequestInfo.error == QWebEngineWebAuthUxRequest.PinEntryError.InternalUvLocked: + errorDetails = self.tr("Internal User Verification Locked ") + elif pinRequestInfo.error == QWebEngineWebAuthUxRequest.PinEntryError.WrongPin: + errorDetails = self.tr("Wrong PIN") + elif pinRequestInfo.error == QWebEngineWebAuthUxRequest.PinEntryError.TooShort: + errorDetails = self.tr("Too Short") + elif pinRequestInfo.error == QWebEngineWebAuthUxRequest.PinEntryError.InvalidCharacters: + errorDetails = self.tr("Invalid Characters") + elif pinRequestInfo.error == QWebEngineWebAuthUxRequest.PinEntryError.SameAsCurrentPin: + errorDetails = self.tr("Same as current PIN") + + if errorDetails: + errorDetails += f" {pinRequestInfo.remainingAttempts} attempts remaining" + + self.uiWebAuthDialog.m_pinEntryErrorLabel.setText(errorDetails) + + def onCancelRequest(self): + + self.uxRequest.cancel() + + def onAcceptRequest(self): + + state = self.uxRequest.state() + if state == QWebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: + if self.button_group.checkedButton(): + self.uxRequest.setSelectedAccount(self.button_group.checkedButton().text()) + elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: + self.uxRequest.setPin(self.uiWebAuthDialog.m_pinLineEdit.text()) + + def setupErrorUI(self): + + self.clearSelectAccountButtons() + error_description = "" + error_heading = self.tr("Something went wrong") + isVisibleRetry = False + + state = self.uxRequest.requestFailureReason() + failure_reason = QWebEngineWebAuthUxRequest.RequestFailureReason + + if state == failure_reason.Timeout: + error_description = self.tr("Request Timeout") + elif state == failure_reason.KeyNotRegistered: + error_description = self.tr("Key not registered") + elif state == failure_reason.KeyAlreadyRegistered: + error_description = self.tr("You already registered self device." + "Try again with device") + isVisibleRetry = True + elif state == failure_reason.SoftPinBlock: + error_description = self.tr( + "The security key is locked because the wrong PIN was entered too many times." + "To unlock it, remove and reinsert it.") + isVisibleRetry = True + elif state == failure_reason.HardPinBlock: + error_description = self.tr( + "The security key is locked because the wrong PIN was entered too many times." + " Yo'll need to reset the security key.") + elif state == failure_reason.AuthenticatorRemovedDuringPinEntry: + error_description = self.tr( + "Authenticator removed during verification. Please reinsert and try again") + elif state == failure_reason.AuthenticatorMissingResidentKeys: + error_description = self.tr("Authenticator doesn't have resident key support") + elif state == failure_reason.AuthenticatorMissingUserVerification: + error_description = self.tr("Authenticator missing user verification") + elif state == failure_reason.AuthenticatorMissingLargeBlob: + error_description = self.tr("Authenticator missing Large Blob support") + elif state == failure_reason.NoCommonAlgorithms: + error_description = self.tr("Authenticator missing Large Blob support") + elif state == failure_reason.StorageFull: + error_description = self.tr("Storage Full") + elif state == failure_reason.UserConsentDenied: + error_description = self.tr("User consent denied") + elif state == failure_reason.WinUserCancelled: + error_description = self.tr("User Cancelled Request") + + self.uiWebAuthDialog.m_headingLabel.setText(error_heading) + self.uiWebAuthDialog.m_description.setText(error_description) + self.uiWebAuthDialog.m_description.adjustSize() + self.uiWebAuthDialog.m_pinGroupBox.setVisible(False) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Ok).setVisible(False) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Retry).setVisible(isVisibleRetry) + if isVisibleRetry: + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Retry).setFocus() + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Cancel).setVisible(True) + self.uiWebAuthDialog.buttonBox.button(QDialogButtonBox.Cancel).setText(self.tr("Close")) + self.scroll_area.setVisible(False) + + def onRetry(self): + self.uxRequest.retry() + + def clearSelectAccountButtons(self): + buttons = self.button_group.buttons() + + for radio_button in buttons: + self.select_account_layout.removeWidget(radio_button) + self.button_group.removeButton(radio_button) + radio_button.deleteLater() diff --git a/examples/webenginewidgets/simplebrowser/webauthdialog.ui b/examples/webenginewidgets/simplebrowser/webauthdialog.ui new file mode 100644 index 00000000..c8a0456d --- /dev/null +++ b/examples/webenginewidgets/simplebrowser/webauthdialog.ui @@ -0,0 +1,151 @@ + + + WebAuthDialog + + + + 0 + 0 + 563 + 397 + + + + Dialog + + + + + 20 + 320 + 471 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Retry + + + + + + 30 + 20 + 321 + 16 + + + + Heading + + + false + + + + + + 30 + 60 + 491 + 31 + + + + Description + + + false + + + + + + 20 + 100 + 471 + 171 + + + + + QLayout::SetDefaultConstraint + + + + + + + + true + + + + + 10 + 20 + 58 + 16 + + + + PIN + + + + + + 90 + 20 + 113 + 21 + + + + + + + 10 + 50 + 81 + 16 + + + + Confirm PIN + + + + + + 90 + 50 + 113 + 21 + + + + + + + 10 + 80 + 441 + 16 + + + + TextLabel + + + + + + + + + + diff --git a/examples/webenginewidgets/simplebrowser/webpopupwindow.py b/examples/webenginewidgets/simplebrowser/webpopupwindow.py index fd82ed9f..5b5ed173 100644 --- a/examples/webenginewidgets/simplebrowser/webpopupwindow.py +++ b/examples/webenginewidgets/simplebrowser/webpopupwindow.py @@ -13,7 +13,6 @@ class WebPopupWindow(QWidget): def __init__(self, view, profile, parent=None): super().__init__(parent, Qt.Window) - self.m_urlLineEdit = QLineEdit(self) self._url_line_edit = QLineEdit() self._fav_action = QAction(self) self._view = view diff --git a/examples/webenginewidgets/simplebrowser/webview.py b/examples/webenginewidgets/simplebrowser/webview.py index c7e57717..4b106acb 100644 --- a/examples/webenginewidgets/simplebrowser/webview.py +++ b/examples/webenginewidgets/simplebrowser/webview.py @@ -5,21 +5,24 @@ from __future__ import annotations from functools import partial from PySide6.QtWebEngineCore import (QWebEngineFileSystemAccessRequest, - QWebEnginePage) + QWebEnginePage, + QWebEngineWebAuthUxRequest) from PySide6.QtWebEngineWidgets import QWebEngineView from PySide6.QtWidgets import QDialog, QMessageBox, QStyle from PySide6.QtGui import QIcon from PySide6.QtNetwork import QAuthenticator -from PySide6.QtCore import QTimer, Signal, Slot +from PySide6.QtCore import QTimer, Signal, Slot, Qt from webpage import WebPage from webpopupwindow import WebPopupWindow from ui_passworddialog import Ui_PasswordDialog from ui_certificateerrordialog import Ui_CertificateErrorDialog +from webauthdialog import WebAuthDialog def question_for_feature(feature): + if feature == QWebEnginePage.Geolocation: return "Allow %1 to access your location information?" if feature == QWebEnginePage.MediaAudioCapture: @@ -59,6 +62,7 @@ class WebView(QWebEngineView): self._loading_icon = QIcon.fromTheme(QIcon.ThemeIcon.ViewRefresh, QIcon(":view-refresh.png")) self._default_icon = QIcon(":text-html.png") + self.auth_dialog = None @Slot() def _load_started(self): @@ -105,6 +109,7 @@ class WebView(QWebEngineView): self.handle_proxy_authentication_required) old_page.registerProtocolHandlerRequested.disconnect( self.handle_register_protocol_handler_requested) + old_page.webAuthUxRequested.disconnect(self.handle_web_auth_ux_requested) old_page.fileSystemAccessRequested.disconnect(self.handle_file_system_access_requested) self.create_web_action_trigger(page, QWebEnginePage.WebAction.Forward) @@ -118,6 +123,7 @@ class WebView(QWebEngineView): page.proxyAuthenticationRequired.connect(self.handle_proxy_authentication_required) page.registerProtocolHandlerRequested.connect( self.handle_register_protocol_handler_requested) + page.webAuthUxRequested.connect(self.handle_web_auth_ux_requested) page.fileSystemAccessRequested.connect(self.handle_file_system_access_requested) def load_progress(self): @@ -265,6 +271,28 @@ class WebView(QWebEngineView): # Set authenticator null if dialog is cancelled auth = QAuthenticator() + def handle_web_auth_ux_requested(self, request): + if self.auth_dialog: + self.auth_dialog.deleteLater() + + self.auth_dialog = WebAuthDialog(request, self.window()) + self.auth_dialog.setModal(False) + self.auth_dialog.setWindowFlags(self.auth_dialog.windowFlags() + & ~Qt.WindowContextHelpButtonHint) + + request.stateChanged.connect(self.on_state_changed) + self.auth_dialog.show() + + def on_state_changed(self, state): + if state in (QWebEngineWebAuthUxRequest.WebAuthUxState.Completed, + QWebEngineWebAuthUxRequest.WebAuthUxState.Cancelled): + if self.auth_dialog: + self.auth_dialog.deleteLater() + self.auth_dialog = None + else: + if self.auth_dialog: + self.auth_dialog.update_display() + def handle_register_protocol_handler_requested(self, request): host = request.origin().host() m = f"Allow {host} to open all {request.scheme()} links?" diff --git a/examples/widgets/itemviews/stardelegate/starrating.py b/examples/widgets/itemviews/stardelegate/starrating.py index 694bb43e..96c08fca 100644 --- a/examples/widgets/itemviews/stardelegate/starrating.py +++ b/examples/widgets/itemviews/stardelegate/starrating.py @@ -6,7 +6,7 @@ from __future__ import annotations from math import (cos, sin, pi) -from PySide6.QtGui import (QPainter, QPolygonF) +from PySide6.QtGui import (QPainter, QPainterStateGuard, QPolygonF) from PySide6.QtCore import (QPointF, QSize, Qt) PAINTING_SCALE_FACTOR = 20 @@ -39,25 +39,22 @@ class StarRating: def paint(self, painter, rect, palette, isEditable=False): """ Paint the stars (and/or diamonds if we're in editing mode). """ - painter.save() - - painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) - painter.setPen(Qt.PenStyle.NoPen) - - if isEditable: - painter.setBrush(palette.highlight()) - else: - painter.setBrush(palette.windowText()) - - y_offset = (rect.height() - PAINTING_SCALE_FACTOR) / 2 - painter.translate(rect.x(), rect.y() + y_offset) - painter.scale(PAINTING_SCALE_FACTOR, PAINTING_SCALE_FACTOR) - - for i in range(self.MAX_STAR_COUNT): - if i < self.star_count: - painter.drawPolygon(self._star_polygon, Qt.FillRule.WindingFill) - elif isEditable: - painter.drawPolygon(self._diamond_polygon, Qt.WindingFill) - painter.translate(1.0, 0.0) - - painter.restore() + with QPainterStateGuard(painter): + painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) + painter.setPen(Qt.NoPen) + + if isEditable: + painter.setBrush(palette.highlight()) + else: + painter.setBrush(palette.windowText()) + + y_offset = (rect.height() - PAINTING_SCALE_FACTOR) / 2 + painter.translate(rect.x(), rect.y() + y_offset) + painter.scale(PAINTING_SCALE_FACTOR, PAINTING_SCALE_FACTOR) + + for i in range(self.MAX_STAR_COUNT): + if i < self.star_count: + painter.drawPolygon(self._star_polygon, Qt.FillRule.WindingFill) + elif isEditable: + painter.drawPolygon(self._diamond_polygon, Qt.FillRule.WindingFill) + painter.translate(1.0, 0.0) diff --git a/examples/widgets/painting/basicdrawing/basicdrawing.py b/examples/widgets/painting/basicdrawing/basicdrawing.py index ef4af5d8..a712c580 100644 --- a/examples/widgets/painting/basicdrawing/basicdrawing.py +++ b/examples/widgets/painting/basicdrawing/basicdrawing.py @@ -7,8 +7,8 @@ from __future__ import annotations from PySide6.QtCore import QPoint, QRect, QSize, Qt, qVersion from PySide6.QtGui import (QBrush, QConicalGradient, QLinearGradient, QPainter, - QPainterPath, QPalette, QPen, QPixmap, QPolygon, - QRadialGradient) + QPainterStateGuard, QPainterPath, QPalette, QPen, + QPixmap, QPolygon, QRadialGradient) from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QGridLayout, QLabel, QSpinBox, QWidget) @@ -86,44 +86,42 @@ class RenderArea(QWidget): for x in range(0, self.width(), 100): for y in range(0, self.height(), 100): - painter.save() - painter.translate(x, y) - if self.transformed: - painter.translate(50, 50) - painter.rotate(60.0) - painter.scale(0.6, 0.9) - painter.translate(-50, -50) - - if self.shape == RenderArea.Line: - painter.drawLine(rect.bottomLeft(), rect.topRight()) - elif self.shape == RenderArea.Points: - painter.drawPoints(RenderArea.points) - elif self.shape == RenderArea.Polyline: - painter.drawPolyline(RenderArea.points) - elif self.shape == RenderArea.Polygon: - painter.drawPolygon(RenderArea.points) - elif self.shape == RenderArea.Rect: - painter.drawRect(rect) - elif self.shape == RenderArea.RoundedRect: - painter.drawRoundedRect(rect, 25, 25, Qt.SizeMode.RelativeSize) - elif self.shape == RenderArea.Ellipse: - painter.drawEllipse(rect) - elif self.shape == RenderArea.Arc: - painter.drawArc(rect, start_angle, arc_length) - elif self.shape == RenderArea.Chord: - painter.drawChord(rect, start_angle, arc_length) - elif self.shape == RenderArea.Pie: - painter.drawPie(rect, start_angle, arc_length) - elif self.shape == RenderArea.Path: - painter.drawPath(path) - elif self.shape == RenderArea.Text: - qv = qVersion() - painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, - f"PySide 6\nQt {qv}") - elif self.shape == RenderArea.Pixmap: - painter.drawPixmap(10, 10, self.pixmap) - - painter.restore() + with QPainterStateGuard(painter): + painter.translate(x, y) + if self.transformed: + painter.translate(50, 50) + painter.rotate(60.0) + painter.scale(0.6, 0.9) + painter.translate(-50, -50) + + if self.shape == RenderArea.Line: + painter.drawLine(rect.bottomLeft(), rect.topRight()) + elif self.shape == RenderArea.Points: + painter.drawPoints(RenderArea.points) + elif self.shape == RenderArea.Polyline: + painter.drawPolyline(RenderArea.points) + elif self.shape == RenderArea.Polygon: + painter.drawPolygon(RenderArea.points) + elif self.shape == RenderArea.Rect: + painter.drawRect(rect) + elif self.shape == RenderArea.RoundedRect: + painter.drawRoundedRect(rect, 25, 25, Qt.SizeMode.RelativeSize) + elif self.shape == RenderArea.Ellipse: + painter.drawEllipse(rect) + elif self.shape == RenderArea.Arc: + painter.drawArc(rect, start_angle, arc_length) + elif self.shape == RenderArea.Chord: + painter.drawChord(rect, start_angle, arc_length) + elif self.shape == RenderArea.Pie: + painter.drawPie(rect, start_angle, arc_length) + elif self.shape == RenderArea.Path: + painter.drawPath(path) + elif self.shape == RenderArea.Text: + qv = qVersion() + painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, + f"PySide 6\nQt {qv}") + elif self.shape == RenderArea.Pixmap: + painter.drawPixmap(10, 10, self.pixmap) painter.setPen(self.palette().dark().color()) painter.setBrush(Qt.BrushStyle.NoBrush) diff --git a/examples/widgets/tutorials/addressbook/addressbook.pyproject b/examples/widgets/tutorials/addressbook/addressbook.pyproject deleted file mode 100644 index 13d739e1..00000000 --- a/examples/widgets/tutorials/addressbook/addressbook.pyproject +++ /dev/null @@ -1,4 +0,0 @@ -{ - "files": ["part3.py", "part1.py", "part5.py", "part2.py", - "part7.py", "part6.py", "part4.py"] -} diff --git a/examples/widgets/tutorials/addressbook/doc/addressbook.png b/examples/widgets/tutorials/addressbook/doc/addressbook.png deleted file mode 100644 index 7d563c17c11a4a01b7ba4db9c101c066c73208d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4989 zcmYkAcQ~BS_xD%dh~6WFRTdEhi)hhSw@M^RbcwnVJ$i|VXc4TsYE}t}5;gipw|Z|8 zB6=sR62Wu({JwuYuj@K<-80ucf6SRV=Y3|P^mWxK$yv!kAP}Xd2K*5SL{JDA6_^MZ z!T(aJ1c3+#^mQJq0$oBvLSkZK5)u+pQc^N9GB6l?{MFkFrtE#H1si~=}t7~Xz zXliO|X=!O|YwPIfAP@*$U0pprJ$-$B0|SFcj~+dF^2Ef%#1tUD>gm&`W@cvQ=H?a_ z7M7NlR#sNl*48#QHnz65c6N65_Vx}A4$jWbNF?&bix)3nzI1bQb9Z<5@bK{T^z`!b z^7i)j@$vEX_4NZNYWFYd2mmPV2n-Ai2?+@e4GjbM-2MjOOGh}smyWk@-$s;lynFX9 zGBPqMDk?fUIwmG2Ha7PC`}c8iaq;o-2?+@(6e=kxDLFYgB_$;_H8m|QEh8f%Gcz+Q zD=RxYJ0~Y6H#Zl9!Q|)X7Zel}78ZW`^r@(*sJOWJ%a<=DB_*Y$rC+~(t*EG|tgNiA zuCA%6`S$HwU0q#$eSKqNV^dR8b8~Y`OA8i@{r>%XYiny;TU&d3dq+n{XJ=+kO$0GL@A92^`1m{}MGnEf>}GBP?kIyN>oK0ZD%F);}+zce*9 zH9b8&Gcz+gJNx6ukGZ+Ip8y;C^Yily02}+i0JaVm7Z-p3{=Kxcw7k5$va+%Yuy?cu zu#aC~U*Fi+*xcOQ0yw~LZ*T7a9G>j%?(XgF?eFg&931@l^XKsJ@aX6WkH;S$AD^6@ zoSvSZot^#t`}h3({NKNS7Z(>-S62tzkv~Bo8l)y%>9OzZPFBFl0WLBMo*ZWW9SOa6qnH;Gq5ueA(CG=VxOfDvTQ@RG;gLcJtQNe1rG9lE7A;k5QMu zwxJ9;CAeSL0_TsS3Pzv!0xo8|1%UlgRit<>c_&43b+o{Uf zb13nv`%i5g?)Cjk6PCjUc4olO`?tI31>o~l^{Ot*r=4hR zGd-G%)}wR7wa7@}AivrRybJTy>fY@Bq^od_4u(i?D&;bC9WHM5oh;2J(?q!x6l%45 z8E1vIGz?ZRgB&C8vd?ay8l%_8wGUcD)Zj;$ocPp#VMBi@#Lr-q>!TVI-eW@=NmquF z;-1>r5k{R0p242AYP7C{`fvn2jAl`VLDMS$V!%dg05&);@ZOr7paNe7LIaQj7dz`8 zvd=PW`n6ELFXp4pNq3{%)W&)?qK}TQJ}W3-b0Q-mmC2$SA|r`0V3pwD;2GVgK6M_Q zjYrFupDh|?r~6*O(*1++coxoGgw6#K!IRs+Yv0Q-BK&$$>T$~jD|SwCk6a!a>?Y9u zy%-P3=*6;BWxKG+INwMs$8fW2mF6xo*h-@RWS`w&C!@-F=r5V`Yr6h;b}~;_bqvSR zrK+y(DlK$5#wQY3{n}A`bJgX*aIcf?L88qmykjk5c?^r`9q4;?^@`vVP1W#oTp#}I z^2V=mL3gDen)g`e3Z|dC-9=Mjwc$s3THdo2yj@2p*@a!A)}}9&>07 z2OF7r!u787{iLtDGnM<*#@urd)B3|v^KZY~DqH0fMta>vv-mi(;xzm`f3tWIDGEkG zEzi( zNfJjj*@0&H&yK)jllqHdth1)5jC(hV%VfhJK5=}8c-h)8p8V}7#l#e4#^SM4It7{R z5D1qNZ+{@LtD-*fBKz6b!ura`4r1v#4!g{}Losjh6bi11nNac}9M04e8$RcAd7rKYk-qPdI1NK!a^ghM@(>a`U;YEk4+)d5Nlljc9FxK~KBwiZ=?oLm` z`G{gFDT3uTG__EEbY=d@wT0W7Ca7_Gk1|Lq*jz=J8i6P)fw8y(OP0Fz0~RR#r!mt^MfY;Pp$M;>M2ybAvb z1OBOlKCKJ(R+K;{HbA*@_`eW+O=3=>0IfZZ{ryP27fV(vgrSs0pKMMIE7l{%%2&kQV2X0U2n3cYiBn8R z05#P+7f!}peS$FdXDy&{qi~;GS&BSM^$4~KIPCES~%+vnOjT9 za;TctImt|FRbW|emE3`rQ?s@fRHeWxvsGUl^x3puICjeYxzT_*_yqEP6G?m1)m5HO z7Y?$OVS9+%=`0xV5=jJw**iUR-xDLDkt`u{JToypm~fw;_kTVbV@08e)aT^vj887< zx*NhNN*<|2xccd~5h(~P)dkY$EfdRk_8UD6hgQ*hFO@hAO4!L7NDZRp;^RFj>v^hV zfNgjS1sbC3jjW59>-#^2zEKQJq) zKn8_-?yTH|&XLkIrGLA&yxd{?ap+3K*#<#3$^#Blyl*z_vFM?B!0Gm_7-^ny=afamI6s4w z(!Lho*k5u?oSn^oR17`}@h+zVEpZ_eC11C>R8syqHhQ|~wI`2u?xk@u^_+N5^LwW> zAKHA{2?7OyFo~cNqgDlPyU|2Gzfn{o)S(w=I+ls_D9p^1@Z*H6v3lTmT(4J)_}^;p zxKY-p{!jk^9lT@X9IaO$`@63Mhe!#3Z)VY@R*@o6PZHA#M1SSG>*#;AQ)+x2{Kn!L z)@Lg(AHFjeOKI>z{A<$ui;nDg$58PVD$wtT5~11&bnd)E))kCq!4?n{&C+~xNy^jo zKp!771Y5e%*y@^kum5n{HV<&wGKhoXoMa9fj-z(EKL$$=U)7K^Q0_i@A?60tL>OWv zsD{1UD;&IM`VjMI9q_`D8v`*Fi@$=B#V0Bv!g;1q9sDP*>aCJD?Zhc*N z<(qQPhW6-2g3<-!#0ve$yo~c{jYeU}+D@rMyJr5LkY&^9*thDf+LHM=m)bcOQR2=3 z4%hh{?eB|5)gfZ9`}~@k@!R>#^FZ-C`e(E$XkzVz|D$~1rjLTF0oTrH!kaxTC2aNg z1{3cq_C?L1JhUy}Vk6Dyq!*HIk_R82iR>vL1rk8mue-*R@y4W8&~Nc2ccpj4v|zXr zoP0iuR0G8`v2sM^8;J^N#D6B=wlBS0`L$yWNE?7(nSfrV^8K=Qiy=`HREAI#o@62O z%hDQxZfblyqeW6aQbN-l#XGLI|DKSw|BUxd8h_o*>7ls1st4G%E;LV`K;0K_Vi2KN zPF!(1jk4_PLC&1Zh~-Zz!j({*V|3hJ+5K;XHV+v-vMBjV4Kqfi@Q3)%**`-hY96&c zGUkI(VGaY`ai=U{owAc7`qQ&OP9T}&DG1C|4%&Nqo36^J7(~9aIZ8eZ-@^N=&>$1Z zOE0J@0=7jPM?d`SigH}s$#%o^yt;`vqVqnc|5=?-N&vXj@jq)tutu zp3=GCnl3_4iQGHgX$rY?U5e-p?7cS<0~^;VAt|)L%!d+W`r1GO#vMxOOk>QX*_p*3 z%@t*Q$4YEo4L(4nUKQ6S^MYQg2E}YeDGQ`ocHMELWX3?8+LNUCX*qK8FZZaUm8RP^ z#YEl3ur!!K=J)@(+_GuJ5yr~sE}$6N%A7yYuSFAT5@dv-Gg+awHb`mxz$Y*8Q&8-O zNPk@<;B_6tTIbH#m>yG9Hw@CAgY1#N7CjYupt%-3vcz(T!sC9ge{?)qKEA&0x zJ@=AyH)V}^y#5J&u?j1n1maFb2%gKWnQ#oR?)w1>iu>_8cYbB&^JkN#QSoS})xDu+N3i z;;?akZ-m7A2N_ZPlen<30TDP0hKgG8vxO4r3rTcXR5ePWb4PyiG=y{ne<^ZKaaP;? zxZ~2HFVdlO+rZ?D9fX$lE#*lb<6#dr=fws58+(`<&76E}_xbk8hiML%U}w`GKX2jH z<$!m9;sq|kz{DRh+8H;%x2YcK23Mv^GMxw;a(>MF6OnS?t))moE#h+uO+DnNZZ z#u!5gt1yNZZhY9s?6~}0hDwT(D=J1q1WhT6-s3RGfnkLN+6VwBD0oM&EAy=;x2G9QlD+KFnm?J)pgk8 z;`-Bf$q+bG4baqMt_M?foUb7oRw%&$+DxQ*{C66ja$G?(cu+!E!x}tlm0>;;^XvKb vdHha2a|V%Z|6gg`@fA7=ScC*F>xzVw9(yk(dSMuNM+0f9>cY#Et;7Bw`kR_( diff --git a/examples/widgets/tutorials/addressbook/doc/addressbook.rst b/examples/widgets/tutorials/addressbook/doc/addressbook.rst deleted file mode 100644 index 646d949d..00000000 --- a/examples/widgets/tutorials/addressbook/doc/addressbook.rst +++ /dev/null @@ -1,9 +0,0 @@ -Address Book Example -==================== - -The address book example shows how to use proxy models to display different -views onto data from a single model. - -.. image:: addressbook.png - :width: 400 - :alt: Address Book Screenshot diff --git a/examples/widgets/tutorials/addressbook/part1.py b/examples/widgets/tutorials/addressbook/part1.py deleted file mode 100644 index c3785f41..00000000 --- a/examples/widgets/tutorials/addressbook/part1.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 2013 Riverbank Computing Limited. -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -import sys - -from PySide6.QtCore import Qt -from PySide6.QtWidgets import (QApplication, QGridLayout, QLabel, QLineEdit, - QTextEdit, QWidget) - - -class AddressBook(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - - name_label = QLabel("Name:") - self._name_line = QLineEdit() - - address_label = QLabel("Address:") - self._address_text = QTextEdit() - - main_layout = QGridLayout() - main_layout.addWidget(name_label, 0, 0) - main_layout.addWidget(self._name_line, 0, 1) - main_layout.addWidget(address_label, 1, 0, Qt.AlignmentFlag.AlignTop) - main_layout.addWidget(self._address_text, 1, 1) - - self.setLayout(main_layout) - self.setWindowTitle("Simple Address Book") - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - address_book = AddressBook() - address_book.show() - - sys.exit(app.exec()) diff --git a/examples/widgets/tutorials/addressbook/part2.py b/examples/widgets/tutorials/addressbook/part2.py deleted file mode 100644 index 04550871..00000000 --- a/examples/widgets/tutorials/addressbook/part2.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright (C) 2013 Riverbank Computing Limited. -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -import sys - -from PySide6.QtCore import Qt -from PySide6.QtWidgets import (QApplication, QGridLayout, - QLabel, QLineEdit, - QMessageBox, QPushButton, QTextEdit, - QVBoxLayout, QWidget) - - -class SortedDict(dict): - class Iterator: - def __init__(self, sorted_dict): - self._dict = sorted_dict - self._keys = sorted(self._dict.keys()) - self._nr_items = len(self._keys) - self._idx = 0 - - def __iter__(self): - return self - - def next(self): - if self._idx >= self._nr_items: - raise StopIteration - - key = self._keys[self._idx] - value = self._dict[key] - self._idx += 1 - - return key, value - - __next__ = next - - def __iter__(self): - return SortedDict.Iterator(self) - - iterkeys = __iter__ - - -class AddressBook(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - - self.contacts = SortedDict() - self._old_name = '' - self._old_address = '' - - name_label = QLabel("Name:") - self._name_line = QLineEdit() - self._name_line.setReadOnly(True) - - address_label = QLabel("Address:") - self._address_text = QTextEdit() - self._address_text.setReadOnly(True) - - self._add_button = QPushButton("&Add") - self._submit_button = QPushButton("&Submit") - self._submit_button.hide() - self._cancel_button = QPushButton("&Cancel") - self._cancel_button.hide() - - self._add_button.clicked.connect(self.add_contact) - self._submit_button.clicked.connect(self.submit_contact) - self._cancel_button.clicked.connect(self.cancel) - - button_layout_1 = QVBoxLayout() - button_layout_1.addWidget(self._add_button, Qt.AlignmentFlag.AlignTop) - button_layout_1.addWidget(self._submit_button) - button_layout_1.addWidget(self._cancel_button) - button_layout_1.addStretch() - - main_layout = QGridLayout() - main_layout.addWidget(name_label, 0, 0) - main_layout.addWidget(self._name_line, 0, 1) - main_layout.addWidget(address_label, 1, 0, Qt.AlignmentFlag.AlignTop) - main_layout.addWidget(self._address_text, 1, 1) - main_layout.addLayout(button_layout_1, 1, 2) - - self.setLayout(main_layout) - self.setWindowTitle("Simple Address Book") - - def add_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(False) - self._name_line.setFocus(Qt.FocusReason.OtherFocusReason) - self._address_text.setReadOnly(False) - - self._add_button.setEnabled(False) - self._submit_button.show() - self._cancel_button.show() - - def submit_contact(self): - name = self._name_line.text() - address = self._address_text.toPlainText() - - if name == "" or address == "": - QMessageBox.information(self, "Empty Field", "Please enter a name and address.") - return - - if name not in self.contacts: - self.contacts[name] = address - QMessageBox.information(self, "Add Successful", - f'"{name}" has been added to your address book.') - else: - QMessageBox.information(self, "Add Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - - if not self.contacts: - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(True) - self._address_text.setReadOnly(True) - self._add_button.setEnabled(True) - self._submit_button.hide() - self._cancel_button.hide() - - def cancel(self): - self._name_line.setText(self._old_name) - self._name_line.setReadOnly(True) - - self._address_text.setText(self._old_address) - self._address_text.setReadOnly(True) - - self._add_button.setEnabled(True) - self._submit_button.hide() - self._cancel_button.hide() - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - address_book = AddressBook() - address_book.show() - - sys.exit(app.exec()) diff --git a/examples/widgets/tutorials/addressbook/part3.py b/examples/widgets/tutorials/addressbook/part3.py deleted file mode 100644 index 5083afcf..00000000 --- a/examples/widgets/tutorials/addressbook/part3.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright (C) 2013 Riverbank Computing Limited. -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -import sys - -from PySide6.QtCore import Qt, Slot -from PySide6.QtWidgets import (QApplication, QGridLayout, - QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QPushButton, QTextEdit, - QVBoxLayout, QWidget) - - -class SortedDict(dict): - class Iterator: - def __init__(self, sorted_dict): - self._dict = sorted_dict - self._keys = sorted(self._dict.keys()) - self._nr_items = len(self._keys) - self._idx = 0 - - def __iter__(self): - return self - - def next(self): - if self._idx >= self._nr_items: - raise StopIteration - - key = self._keys[self._idx] - value = self._dict[key] - self._idx += 1 - - return key, value - - __next__ = next - - def __iter__(self): - return SortedDict.Iterator(self) - - iterkeys = __iter__ - - -class AddressBook(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - - self.contacts = SortedDict() - self._old_name = '' - self._old_address = '' - - name_label = QLabel("Name:") - self._name_line = QLineEdit() - self._name_line.setReadOnly(True) - - address_label = QLabel("Address:") - self._address_text = QTextEdit() - self._address_text.setReadOnly(True) - - self._add_button = QPushButton("&Add") - self._submit_button = QPushButton("&Submit") - self._submit_button.hide() - self._cancel_button = QPushButton("&Cancel") - self._cancel_button.hide() - self._next_button = QPushButton("&Next") - self._next_button.setEnabled(False) - self._previous_button = QPushButton("&Previous") - self._previous_button.setEnabled(False) - - self._add_button.clicked.connect(self.add_contact) - self._submit_button.clicked.connect(self.submit_contact) - self._cancel_button.clicked.connect(self.cancel) - self._next_button.clicked.connect(self.next) - self._previous_button.clicked.connect(self.previous) - - button_layout_1 = QVBoxLayout() - button_layout_1.addWidget(self._add_button, Qt.AlignmentFlag.AlignTop) - button_layout_1.addWidget(self._submit_button) - button_layout_1.addWidget(self._cancel_button) - button_layout_1.addStretch() - - button_layout_2 = QHBoxLayout() - button_layout_2.addWidget(self._previous_button) - button_layout_2.addWidget(self._next_button) - - main_layout = QGridLayout() - main_layout.addWidget(name_label, 0, 0) - main_layout.addWidget(self._name_line, 0, 1) - main_layout.addWidget(address_label, 1, 0, Qt.AlignmentFlag.AlignTop) - main_layout.addWidget(self._address_text, 1, 1) - main_layout.addLayout(button_layout_1, 1, 2) - main_layout.addLayout(button_layout_2, 3, 1) - - self.setLayout(main_layout) - self.setWindowTitle("Simple Address Book") - - def add_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(False) - self._name_line.setFocus(Qt.FocusReason.OtherFocusReason) - self._address_text.setReadOnly(False) - - self._add_button.setEnabled(False) - self._next_button.setEnabled(False) - self._previous_button.setEnabled(False) - self._submit_button.show() - self._cancel_button.show() - - @Slot() - def submit_contact(self): - name = self._name_line.text() - address = self._address_text.toPlainText() - - if name == "" or address == "": - QMessageBox.information(self, "Empty Field", "Please enter a name and address.") - return - - if name not in self.contacts: - self.contacts[name] = address - QMessageBox.information(self, "Add Successful", - f'"{name}" has been added to your address book.') - else: - QMessageBox.information(self, "Add Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - - if not self.contacts: - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(True) - self._address_text.setReadOnly(True) - self._add_button.setEnabled(True) - - number = len(self.contacts) - self._next_button.setEnabled(number > 1) - self._previous_button.setEnabled(number > 1) - - self._submit_button.hide() - self._cancel_button.hide() - - @Slot() - def cancel(self): - self._name_line.setText(self._old_name) - self._address_text.setText(self._old_address) - - if not self.contacts: - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(True) - self._address_text.setReadOnly(True) - self._add_button.setEnabled(True) - - number = len(self.contacts) - self._next_button.setEnabled(number > 1) - self._previous_button.setEnabled(number > 1) - - self._submit_button.hide() - self._cancel_button.hide() - - @Slot() - def next(self): - name = self._name_line.text() - it = iter(self.contacts) - - try: - while True: - this_name, _ = it.next() - - if this_name == name: - next_name, next_address = it.next() - break - except StopIteration: - next_name, next_address = iter(self.contacts).next() - - self._name_line.setText(next_name) - self._address_text.setText(next_address) - - @Slot() - def previous(self): - name = self._name_line.text() - - prev_name = prev_address = None - for this_name, this_address in self.contacts: - if this_name == name: - break - - prev_name = this_name - prev_address = this_address - else: - self._name_line.clear() - self._address_text.clear() - return - - if prev_name is None: - for prev_name, prev_address in self.contacts: - pass - - self._name_line.setText(prev_name) - self._address_text.setText(prev_address) - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - address_book = AddressBook() - address_book.show() - - sys.exit(app.exec()) diff --git a/examples/widgets/tutorials/addressbook/part4.py b/examples/widgets/tutorials/addressbook/part4.py deleted file mode 100644 index de4bbfac..00000000 --- a/examples/widgets/tutorials/addressbook/part4.py +++ /dev/null @@ -1,272 +0,0 @@ -# Copyright (C) 2013 Riverbank Computing Limited. -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -import sys - -from PySide6.QtCore import Qt, Slot -from PySide6.QtWidgets import (QApplication, QGridLayout, - QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QPushButton, QTextEdit, - QVBoxLayout, QWidget) - - -class SortedDict(dict): - class Iterator: - def __init__(self, sorted_dict): - self._dict = sorted_dict - self._keys = sorted(self._dict.keys()) - self._nr_items = len(self._keys) - self._idx = 0 - - def __iter__(self): - return self - - def next(self): - if self._idx >= self._nr_items: - raise StopIteration - - key = self._keys[self._idx] - value = self._dict[key] - self._idx += 1 - - return key, value - - __next__ = next - - def __iter__(self): - return SortedDict.Iterator(self) - - iterkeys = __iter__ - - -class AddressBook(QWidget): - NavigationMode, AddingMode, EditingMode = range(3) - - def __init__(self, parent=None): - super().__init__(parent) - - self.contacts = SortedDict() - self._old_name = '' - self._old_address = '' - self._current_mode = self.NavigationMode - - name_label = QLabel("Name:") - self._name_line = QLineEdit() - self._name_line.setReadOnly(True) - - address_label = QLabel("Address:") - self._address_text = QTextEdit() - self._address_text.setReadOnly(True) - - self._add_button = QPushButton("&Add") - self._edit_button = QPushButton("&Edit") - self._edit_button.setEnabled(False) - self._remove_button = QPushButton("&Remove") - self._remove_button.setEnabled(False) - self._submit_button = QPushButton("&Submit") - self._submit_button.hide() - self._cancel_button = QPushButton("&Cancel") - self._cancel_button.hide() - - self._next_button = QPushButton("&Next") - self._next_button.setEnabled(False) - self._previous_button = QPushButton("&Previous") - self._previous_button.setEnabled(False) - - self._add_button.clicked.connect(self.add_contact) - self._submit_button.clicked.connect(self.submit_contact) - self._edit_button.clicked.connect(self.edit_contact) - self._remove_button.clicked.connect(self.remove_contact) - self._cancel_button.clicked.connect(self.cancel) - self._next_button.clicked.connect(self.next) - self._previous_button.clicked.connect(self.previous) - - button_layout_1 = QVBoxLayout() - button_layout_1.addWidget(self._add_button) - button_layout_1.addWidget(self._edit_button) - button_layout_1.addWidget(self._remove_button) - button_layout_1.addWidget(self._submit_button) - button_layout_1.addWidget(self._cancel_button) - button_layout_1.addStretch() - - button_layout_2 = QHBoxLayout() - button_layout_2.addWidget(self._previous_button) - button_layout_2.addWidget(self._next_button) - - main_layout = QGridLayout() - main_layout.addWidget(name_label, 0, 0) - main_layout.addWidget(self._name_line, 0, 1) - main_layout.addWidget(address_label, 1, 0, Qt.AlignmentFlag.AlignTop) - main_layout.addWidget(self._address_text, 1, 1) - main_layout.addLayout(button_layout_1, 1, 2) - main_layout.addLayout(button_layout_2, 3, 1) - - self.setLayout(main_layout) - self.setWindowTitle("Simple Address Book") - - @Slot() - def add_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self._name_line.clear() - self._address_text.clear() - - self.update_interface(self.AddingMode) - - @Slot() - def edit_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self.update_interface(self.EditingMode) - - @Slot() - def submit_contact(self): - name = self._name_line.text() - address = self._address_text.toPlainText() - - if name == "" or address == "": - QMessageBox.information(self, "Empty Field", "Please enter a name and address.") - return - - if self._current_mode == self.AddingMode: - if name not in self.contacts: - self.contacts[name] = address - QMessageBox.information(self, "Add Successful", - f'"{name}" has been added to your address book.') - else: - QMessageBox.information(self, "Add Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - - elif self._current_mode == self.EditingMode: - if self._old_name != name: - if name not in self.contacts: - QMessageBox.information(self, "Edit Successful", - f'"{self.oldName}" has been edited in your ' - 'address book.') - del self.contacts[self._old_name] - self.contacts[name] = address - else: - QMessageBox.information(self, "Edit Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - elif self._old_address != address: - QMessageBox.information(self, "Edit Successful", - f'"{name}" has been edited in your address book.') - self.contacts[name] = address - - self.update_interface(self.NavigationMode) - - @Slot() - def cancel(self): - self._name_line.setText(self._old_name) - self._address_text.setText(self._old_address) - self.update_interface(self.NavigationMode) - - @Slot() - def remove_contact(self): - name = self._name_line.text() - - if name in self.contacts: - button = QMessageBox.question(self, "Confirm Remove", - f'Are you sure you want to remove "{name}"?', - QMessageBox.Yes | QMessageBox.No) - - if button == QMessageBox.Yes: - self.previous() - del self.contacts[name] - - QMessageBox.information(self, "Remove Successful", - f'"{name}" has been removed from your address book.') - - self.update_interface(self.NavigationMode) - - @Slot() - def next(self): - name = self._name_line.text() - it = iter(self.contacts) - - try: - while True: - this_name, _ = it.next() - - if this_name == name: - next_name, next_address = it.next() - break - except StopIteration: - next_name, next_address = iter(self.contacts).next() - - self._name_line.setText(next_name) - self._address_text.setText(next_address) - - @Slot() - def previous(self): - name = self._name_line.text() - - prev_name = prev_address = None - for this_name, this_address in self.contacts: - if this_name == name: - break - - prev_name = this_name - prev_address = this_address - else: - self._name_line.clear() - self._address_text.clear() - return - - if prev_name is None: - for prev_name, prev_address in self.contacts: - pass - - self._name_line.setText(prev_name) - self._address_text.setText(prev_address) - - def update_interface(self, mode): - self._current_mode = mode - - if self._current_mode in (self.AddingMode, self.EditingMode): - self._name_line.setReadOnly(False) - self._name_line.setFocus(Qt.FocusReason.OtherFocusReason) - self._address_text.setReadOnly(False) - - self._add_button.setEnabled(False) - self._edit_button.setEnabled(False) - self._remove_button.setEnabled(False) - - self._next_button.setEnabled(False) - self._previous_button.setEnabled(False) - - self._submit_button.show() - self._cancel_button.show() - - elif self._current_mode == self.NavigationMode: - if not self.contacts: - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(True) - self._address_text.setReadOnly(True) - self._add_button.setEnabled(True) - - number = len(self.contacts) - self._edit_button.setEnabled(number >= 1) - self._remove_button.setEnabled(number >= 1) - self._next_button.setEnabled(number > 1) - self._previous_button.setEnabled(number > 1) - - self._submit_button.hide() - self._cancel_button.hide() - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - address_book = AddressBook() - address_book.show() - - sys.exit(app.exec()) diff --git a/examples/widgets/tutorials/addressbook/part5.py b/examples/widgets/tutorials/addressbook/part5.py deleted file mode 100644 index 13538207..00000000 --- a/examples/widgets/tutorials/addressbook/part5.py +++ /dev/null @@ -1,331 +0,0 @@ -# Copyright (C) 2013 Riverbank Computing Limited. -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -import sys - -from PySide6.QtCore import Qt, Slot -from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, - QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QPushButton, QTextEdit, - QVBoxLayout, QWidget) - - -class SortedDict(dict): - class Iterator: - def __init__(self, sorted_dict): - self._dict = sorted_dict - self._keys = sorted(self._dict.keys()) - self._nr_items = len(self._keys) - self._idx = 0 - - def __iter__(self): - return self - - def next(self): - if self._idx >= self._nr_items: - raise StopIteration - - key = self._keys[self._idx] - value = self._dict[key] - self._idx += 1 - - return key, value - - __next__ = next - - def __iter__(self): - return SortedDict.Iterator(self) - - iterkeys = __iter__ - - -class AddressBook(QWidget): - NavigationMode, AddingMode, EditingMode = range(3) - - def __init__(self, parent=None): - super().__init__(parent) - - self.contacts = SortedDict() - self._old_name = '' - self._old_address = '' - self._current_mode = self.NavigationMode - - name_label = QLabel("Name:") - self._name_line = QLineEdit() - self._name_line.setReadOnly(True) - - address_label = QLabel("Address:") - self._address_text = QTextEdit() - self._address_text.setReadOnly(True) - - self._add_button = QPushButton("&Add") - self._edit_button = QPushButton("&Edit") - self._edit_button.setEnabled(False) - self._remove_button = QPushButton("&Remove") - self._remove_button.setEnabled(False) - self._find_button = QPushButton("&Find") - self._find_button.setEnabled(False) - self._submit_button = QPushButton("&Submit") - self._submit_button.hide() - self._cancel_button = QPushButton("&Cancel") - self._cancel_button.hide() - - self._next_button = QPushButton("&Next") - self._next_button.setEnabled(False) - self._previous_button = QPushButton("&Previous") - self._previous_button.setEnabled(False) - - self.dialog = FindDialog() - - self._add_button.clicked.connect(self.add_contact) - self._submit_button.clicked.connect(self.submit_contact) - self._edit_button.clicked.connect(self.edit_contact) - self._remove_button.clicked.connect(self.remove_contact) - self._find_button.clicked.connect(self.find_contact) - self._cancel_button.clicked.connect(self.cancel) - self._next_button.clicked.connect(self.next) - self._previous_button.clicked.connect(self.previous) - - button_layout_1 = QVBoxLayout() - button_layout_1.addWidget(self._add_button) - button_layout_1.addWidget(self._edit_button) - button_layout_1.addWidget(self._remove_button) - button_layout_1.addWidget(self._find_button) - button_layout_1.addWidget(self._submit_button) - button_layout_1.addWidget(self._cancel_button) - button_layout_1.addStretch() - - button_layout_2 = QHBoxLayout() - button_layout_2.addWidget(self._previous_button) - button_layout_2.addWidget(self._next_button) - - main_layout = QGridLayout() - main_layout.addWidget(name_label, 0, 0) - main_layout.addWidget(self._name_line, 0, 1) - main_layout.addWidget(address_label, 1, 0, Qt.AlignmentFlag.AlignTop) - main_layout.addWidget(self._address_text, 1, 1) - main_layout.addLayout(button_layout_1, 1, 2) - main_layout.addLayout(button_layout_2, 2, 1) - - self.setLayout(main_layout) - self.setWindowTitle("Simple Address Book") - - @Slot() - def add_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self._name_line.clear() - self._address_text.clear() - - self.update_interface(self.AddingMode) - - @Slot() - def edit_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self.update_interface(self.EditingMode) - - @Slot() - def submit_contact(self): - name = self._name_line.text() - address = self._address_text.toPlainText() - - if name == "" or address == "": - QMessageBox.information(self, "Empty Field", "Please enter a name and address.") - return - - if self._current_mode == self.AddingMode: - if name not in self.contacts: - self.contacts[name] = address - QMessageBox.information(self, "Add Successful", - f'"{name}" has been added to your address book.') - else: - QMessageBox.information(self, "Add Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - - elif self._current_mode == self.EditingMode: - if self._old_name != name: - if name not in self.contacts: - QMessageBox.information(self, "Edit Successful", - f'"{self.oldName}" has been edited in your ' - 'address book.') - del self.contacts[self._old_name] - self.contacts[name] = address - else: - QMessageBox.information(self, "Edit Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - elif self._old_address != address: - QMessageBox.information(self, "Edit Successful", - f'"{name}" has been edited in your address book.') - self.contacts[name] = address - - self.update_interface(self.NavigationMode) - - @Slot() - def cancel(self): - self._name_line.setText(self._old_name) - self._address_text.setText(self._old_address) - self.update_interface(self.NavigationMode) - - @Slot() - def remove_contact(self): - name = self._name_line.text() - - if name in self.contacts: - button = QMessageBox.question(self, "Confirm Remove", - f'Are you sure you want to remove "{name}"?', - QMessageBox.Yes | QMessageBox.No) - - if button == QMessageBox.Yes: - self.previous() - del self.contacts[name] - - QMessageBox.information(self, "Remove Successful", - f'"{name}" has been removed from your address book.') - - self.update_interface(self.NavigationMode) - - @Slot() - def next(self): - name = self._name_line.text() - it = iter(self.contacts) - - try: - while True: - this_name, _ = it.next() - - if this_name == name: - next_name, next_address = it.next() - break - except StopIteration: - next_name, next_address = iter(self.contacts).next() - - self._name_line.setText(next_name) - self._address_text.setText(next_address) - - @Slot() - def previous(self): - name = self._name_line.text() - - prev_name = prev_address = None - for this_name, this_address in self.contacts: - if this_name == name: - break - - prev_name = this_name - prev_address = this_address - else: - self._name_line.clear() - self._address_text.clear() - return - - if prev_name is None: - for prev_name, prev_address in self.contacts: - pass - - self._name_line.setText(prev_name) - self._address_text.setText(prev_address) - - def find_contact(self): - self.dialog.show() - - if self.dialog.exec() == QDialog.Accepted: - contact_name = self.dialog.get_find_text() - - if contact_name in self.contacts: - self._name_line.setText(contact_name) - self._address_text.setText(self.contacts[contact_name]) - else: - QMessageBox.information(self, "Contact Not Found", - f'Sorry, "{contact_name}" is not in your address book.') - return - - self.update_interface(self.NavigationMode) - - def update_interface(self, mode): - self._current_mode = mode - - if self._current_mode in (self.AddingMode, self.EditingMode): - self._name_line.setReadOnly(False) - self._name_line.setFocus(Qt.FocusReason.OtherFocusReason) - self._address_text.setReadOnly(False) - - self._add_button.setEnabled(False) - self._edit_button.setEnabled(False) - self._remove_button.setEnabled(False) - - self._next_button.setEnabled(False) - self._previous_button.setEnabled(False) - - self._submit_button.show() - self._cancel_button.show() - - elif self._current_mode == self.NavigationMode: - if not self.contacts: - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(True) - self._address_text.setReadOnly(True) - self._add_button.setEnabled(True) - - number = len(self.contacts) - self._edit_button.setEnabled(number >= 1) - self._remove_button.setEnabled(number >= 1) - self._find_button.setEnabled(number > 2) - self._next_button.setEnabled(number > 1) - self._previous_button.setEnabled(number > 1) - - self._submit_button.hide() - self._cancel_button.hide() - - -class FindDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - - find_label = QLabel("Enter the name of a contact:") - self._line_edit = QLineEdit() - - self._find_button = QPushButton("&Find") - self._find_text = '' - - layout = QHBoxLayout() - layout.addWidget(find_label) - layout.addWidget(self._line_edit) - layout.addWidget(self._find_button) - - self.setLayout(layout) - self.setWindowTitle("Find a Contact") - - self._find_button.clicked.connect(self.find_clicked) - self._find_button.clicked.connect(self.accept) - - def find_clicked(self): - text = self._line_edit.text() - - if not text: - QMessageBox.information(self, "Empty Field", "Please enter a name.") - return - else: - self._find_text = text - self._line_edit.clear() - self.hide() - - def get_find_text(self): - return self._find_text - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - address_book = AddressBook() - address_book.show() - - sys.exit(app.exec()) diff --git a/examples/widgets/tutorials/addressbook/part6.py b/examples/widgets/tutorials/addressbook/part6.py deleted file mode 100644 index 73dcd067..00000000 --- a/examples/widgets/tutorials/addressbook/part6.py +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright (C) 2013 Riverbank Computing Limited. -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -import pickle -import sys - -from PySide6.QtCore import Qt, Slot -from PySide6.QtWidgets import (QApplication, QDialog, QFileDialog, - QGridLayout, QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QPushButton, QTextEdit, - QVBoxLayout, QWidget) - - -class SortedDict(dict): - class Iterator: - def __init__(self, sorted_dict): - self._dict = sorted_dict - self._keys = sorted(self._dict.keys()) - self._nr_items = len(self._keys) - self._idx = 0 - - def __iter__(self): - return self - - def next(self): - if self._idx >= self._nr_items: - raise StopIteration - - key = self._keys[self._idx] - value = self._dict[key] - self._idx += 1 - - return key, value - - __next__ = next - - def __iter__(self): - return SortedDict.Iterator(self) - - iterkeys = __iter__ - - -class AddressBook(QWidget): - NavigationMode, AddingMode, EditingMode = range(3) - - def __init__(self, parent=None): - super().__init__(parent) - - self.contacts = SortedDict() - self._old_name = '' - self._old_address = '' - self._current_mode = self.NavigationMode - - name_label = QLabel("Name:") - self._name_line = QLineEdit() - self._name_line.setReadOnly(True) - - address_label = QLabel("Address:") - self._address_text = QTextEdit() - self._address_text.setReadOnly(True) - - self._add_button = QPushButton("&Add") - self._edit_button = QPushButton("&Edit") - self._edit_button.setEnabled(False) - self._remove_button = QPushButton("&Remove") - self._remove_button.setEnabled(False) - self._find_button = QPushButton("&Find") - self._find_button.setEnabled(False) - self._submit_button = QPushButton("&Submit") - self._submit_button.hide() - self._cancel_button = QPushButton("&Cancel") - self._cancel_button.hide() - - self._next_button = QPushButton("&Next") - self._next_button.setEnabled(False) - self._previous_button = QPushButton("&Previous") - self._previous_button.setEnabled(False) - - self._load_button = QPushButton("&Load...") - self._load_button.setToolTip("Load contacts from a file") - self._save_button = QPushButton("Sa&ve...") - self._save_button.setToolTip("Save contacts to a file") - self._save_button.setEnabled(False) - - self.dialog = FindDialog() - - self._add_button.clicked.connect(self.add_contact) - self._submit_button.clicked.connect(self.submit_contact) - self._edit_button.clicked.connect(self.edit_contact) - self._remove_button.clicked.connect(self.remove_contact) - self._find_button.clicked.connect(self.find_contact) - self._cancel_button.clicked.connect(self.cancel) - self._next_button.clicked.connect(self.next) - self._previous_button.clicked.connect(self.previous) - self._load_button.clicked.connect(self.load_from_file) - self._save_button.clicked.connect(self.save_to_file) - - button_layout_1 = QVBoxLayout() - button_layout_1.addWidget(self._add_button) - button_layout_1.addWidget(self._edit_button) - button_layout_1.addWidget(self._remove_button) - button_layout_1.addWidget(self._find_button) - button_layout_1.addWidget(self._submit_button) - button_layout_1.addWidget(self._cancel_button) - button_layout_1.addWidget(self._load_button) - button_layout_1.addWidget(self._save_button) - button_layout_1.addStretch() - - button_layout_2 = QHBoxLayout() - button_layout_2.addWidget(self._previous_button) - button_layout_2.addWidget(self._next_button) - - main_layout = QGridLayout() - main_layout.addWidget(name_label, 0, 0) - main_layout.addWidget(self._name_line, 0, 1) - main_layout.addWidget(address_label, 1, 0, Qt.AlignmentFlag.AlignTop) - main_layout.addWidget(self._address_text, 1, 1) - main_layout.addLayout(button_layout_1, 1, 2) - main_layout.addLayout(button_layout_2, 2, 1) - - self.setLayout(main_layout) - self.setWindowTitle("Simple Address Book") - - @Slot() - def add_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self._name_line.clear() - self._address_text.clear() - - self.update_interface(self.AddingMode) - - @Slot() - def edit_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self.update_interface(self.EditingMode) - - @Slot() - def submit_contact(self): - name = self._name_line.text() - address = self._address_text.toPlainText() - - if name == "" or address == "": - QMessageBox.information(self, "Empty Field", "Please enter a name and address.") - return - - if self._current_mode == self.AddingMode: - if name not in self.contacts: - self.contacts[name] = address - QMessageBox.information(self, "Add Successful", - f'"{name}" has been added to your address book.') - else: - QMessageBox.information(self, "Add Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - - elif self._current_mode == self.EditingMode: - if self._old_name != name: - if name not in self.contacts: - QMessageBox.information(self, "Edit Successful", - f'"{self.oldName}" has been edited in your ' - 'address book.') - del self.contacts[self._old_name] - self.contacts[name] = address - else: - QMessageBox.information(self, "Edit Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - elif self._old_address != address: - QMessageBox.information(self, "Edit Successful", - f'"{name}" has been edited in your address book.') - self.contacts[name] = address - - self.update_interface(self.NavigationMode) - - @Slot() - def cancel(self): - self._name_line.setText(self._old_name) - self._address_text.setText(self._old_address) - self.update_interface(self.NavigationMode) - - @Slot() - def remove_contact(self): - name = self._name_line.text() - - if name in self.contacts: - button = QMessageBox.question(self, "Confirm Remove", - f'Are you sure you want to remove "{name}"?', - QMessageBox.Yes | QMessageBox.No) - - if button == QMessageBox.Yes: - self.previous() - del self.contacts[name] - - QMessageBox.information(self, "Remove Successful", - f'"{name}" has been removed from your address book.') - - self.update_interface(self.NavigationMode) - - @Slot() - def next(self): - name = self._name_line.text() - it = iter(self.contacts) - - try: - while True: - this_name, _ = it.next() - - if this_name == name: - next_name, next_address = it.next() - break - except StopIteration: - next_name, next_address = iter(self.contacts).next() - - self._name_line.setText(next_name) - self._address_text.setText(next_address) - - @Slot() - def previous(self): - name = self._name_line.text() - - prev_name = prev_address = None - for this_name, this_address in self.contacts: - if this_name == name: - break - - prev_name = this_name - prev_address = this_address - else: - self._name_line.clear() - self._address_text.clear() - return - - if prev_name is None: - for prev_name, prev_address in self.contacts: - pass - - self._name_line.setText(prev_name) - self._address_text.setText(prev_address) - - def find_contact(self): - self.dialog.show() - - if self.dialog.exec() == QDialog.Accepted: - contact_name = self.dialog.get_find_text() - - if contact_name in self.contacts: - self._name_line.setText(contact_name) - self._address_text.setText(self.contacts[contact_name]) - else: - QMessageBox.information(self, "Contact Not Found", - f'Sorry, "{contact_name}" is not in your address book.') - return - - self.update_interface(self.NavigationMode) - - def update_interface(self, mode): - self._current_mode = mode - - if self._current_mode in (self.AddingMode, self.EditingMode): - self._name_line.setReadOnly(False) - self._name_line.setFocus(Qt.FocusReason.OtherFocusReason) - self._address_text.setReadOnly(False) - - self._add_button.setEnabled(False) - self._edit_button.setEnabled(False) - self._remove_button.setEnabled(False) - - self._next_button.setEnabled(False) - self._previous_button.setEnabled(False) - - self._submit_button.show() - self._cancel_button.show() - - self._load_button.setEnabled(False) - self._save_button.setEnabled(False) - - elif self._current_mode == self.NavigationMode: - if not self.contacts: - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(True) - self._address_text.setReadOnly(True) - self._add_button.setEnabled(True) - - number = len(self.contacts) - self._edit_button.setEnabled(number >= 1) - self._remove_button.setEnabled(number >= 1) - self._find_button.setEnabled(number > 2) - self._next_button.setEnabled(number > 1) - self._previous_button.setEnabled(number > 1) - - self._submit_button.hide() - self._cancel_button.hide() - - self._load_button.setEnabled(True) - self._save_button.setEnabled(number >= 1) - - def save_to_file(self): - fileName, _ = QFileDialog.getSaveFileName(self, - "Save Address Book", '', - "Address Book (*.abk);;All Files (*)") - - if not fileName: - return - - try: - out_file = open(str(fileName), 'wb') - except IOError: - QMessageBox.information(self, "Unable to open file", - f'There was an error opening "{fileName}"') - return - - pickle.dump(self.contacts, out_file) - out_file.close() - - def load_from_file(self): - fileName, _ = QFileDialog.getOpenFileName(self, - "Open Address Book", '', - "Address Book (*.abk);;All Files (*)") - - if not fileName: - return - - try: - in_file = open(str(fileName), 'rb') - except IOError: - QMessageBox.information(self, "Unable to open file", - f'There was an error opening "{fileName}"') - return - - self.contacts = pickle.load(in_file) - in_file.close() - - if len(self.contacts) == 0: - QMessageBox.information(self, "No contacts in file", - "The file you are attempting to open contains no contacts.") - else: - for name, address in self.contacts: - self._name_line.setText(name) - self._address_text.setText(address) - - self.update_interface(self.NavigationMode) - - -class FindDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - - find_label = QLabel("Enter the name of a contact:") - self._line_edit = QLineEdit() - - self._find_button = QPushButton("&Find") - self._find_text = '' - - layout = QHBoxLayout() - layout.addWidget(find_label) - layout.addWidget(self._line_edit) - layout.addWidget(self._find_button) - - self.setLayout(layout) - self.setWindowTitle("Find a Contact") - - self._find_button.clicked.connect(self.find_clicked) - self._find_button.clicked.connect(self.accept) - - def find_clicked(self): - text = self._line_edit.text() - - if not text: - QMessageBox.information(self, "Empty Field", "Please enter a name.") - return - - self._find_text = text - self._line_edit.clear() - self.hide() - - def get_find_text(self): - return self._find_text - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - address_book = AddressBook() - address_book.show() - - sys.exit(app.exec()) diff --git a/examples/widgets/tutorials/addressbook/part7.py b/examples/widgets/tutorials/addressbook/part7.py deleted file mode 100644 index 12e20557..00000000 --- a/examples/widgets/tutorials/addressbook/part7.py +++ /dev/null @@ -1,445 +0,0 @@ -# Copyright (C) 2013 Riverbank Computing Limited. -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from __future__ import annotations - -import pickle -import sys - -from PySide6.QtCore import QFile, QIODevice, QTextStream, Qt, Slot -from PySide6.QtWidgets import (QApplication, QDialog, QFileDialog, - QGridLayout, QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QPushButton, QTextEdit, - QVBoxLayout, QWidget) - - -class SortedDict(dict): - class Iterator: - def __init__(self, sorted_dict): - self._dict = sorted_dict - self._keys = sorted(self._dict.keys()) - self._nr_items = len(self._keys) - self._idx = 0 - - def __iter__(self): - return self - - def next(self): - if self._idx >= self._nr_items: - raise StopIteration - - key = self._keys[self._idx] - value = self._dict[key] - self._idx += 1 - - return key, value - - __next__ = next - - def __iter__(self): - return SortedDict.Iterator(self) - - iterkeys = __iter__ - - -class AddressBook(QWidget): - NavigationMode, AddingMode, EditingMode = range(3) - - def __init__(self, parent=None): - super().__init__(parent) - - self.contacts = SortedDict() - self._old_name = '' - self._old_address = '' - self._current_mode = self.NavigationMode - - name_label = QLabel("Name:") - self._name_line = QLineEdit() - self._name_line.setReadOnly(True) - - address_label = QLabel("Address:") - self._address_text = QTextEdit() - self._address_text.setReadOnly(True) - - self._add_button = QPushButton("&Add") - self._edit_button = QPushButton("&Edit") - self._edit_button.setEnabled(False) - self._remove_button = QPushButton("&Remove") - self._remove_button.setEnabled(False) - self._find_button = QPushButton("&Find") - self._find_button.setEnabled(False) - self._submit_button = QPushButton("&Submit") - self._submit_button.hide() - self._cancel_button = QPushButton("&Cancel") - self._cancel_button.hide() - - self._next_button = QPushButton("&Next") - self._next_button.setEnabled(False) - self._previous_button = QPushButton("&Previous") - self._previous_button.setEnabled(False) - - self._load_button = QPushButton("&Load...") - self._load_button.setToolTip("Load contacts from a file") - self._save_button = QPushButton("Sa&ve...") - self._save_button.setToolTip("Save contacts to a file") - self._save_button.setEnabled(False) - - self._export_button = QPushButton("Ex&port") - self._export_button.setToolTip("Export as vCard") - self._export_button.setEnabled(False) - - self.dialog = FindDialog() - - self._add_button.clicked.connect(self.add_contact) - self._submit_button.clicked.connect(self.submit_contact) - self._edit_button.clicked.connect(self.edit_contact) - self._remove_button.clicked.connect(self.remove_contact) - self._find_button.clicked.connect(self.find_contact) - self._cancel_button.clicked.connect(self.cancel) - self._next_button.clicked.connect(self.next) - self._previous_button.clicked.connect(self.previous) - self._load_button.clicked.connect(self.load_from_file) - self._save_button.clicked.connect(self.save_to_file) - self._export_button.clicked.connect(self.export_as_vcard) - - button_layout_1 = QVBoxLayout() - button_layout_1.addWidget(self._add_button) - button_layout_1.addWidget(self._edit_button) - button_layout_1.addWidget(self._remove_button) - button_layout_1.addWidget(self._find_button) - button_layout_1.addWidget(self._submit_button) - button_layout_1.addWidget(self._cancel_button) - button_layout_1.addWidget(self._load_button) - button_layout_1.addWidget(self._save_button) - button_layout_1.addWidget(self._export_button) - button_layout_1.addStretch() - - button_layout_2 = QHBoxLayout() - button_layout_2.addWidget(self._previous_button) - button_layout_2.addWidget(self._next_button) - - main_layout = QGridLayout() - main_layout.addWidget(name_label, 0, 0) - main_layout.addWidget(self._name_line, 0, 1) - main_layout.addWidget(address_label, 1, 0, Qt.AlignmentFlag.AlignTop) - main_layout.addWidget(self._address_text, 1, 1) - main_layout.addLayout(button_layout_1, 1, 2) - main_layout.addLayout(button_layout_2, 2, 1) - - self.setLayout(main_layout) - self.setWindowTitle("Simple Address Book") - - @Slot() - def add_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self._name_line.clear() - self._address_text.clear() - - self.update_interface(self.AddingMode) - - @Slot() - def edit_contact(self): - self._old_name = self._name_line.text() - self._old_address = self._address_text.toPlainText() - - self.update_interface(self.EditingMode) - - @Slot() - def submit_contact(self): - name = self._name_line.text() - address = self._address_text.toPlainText() - - if name == "" or address == "": - QMessageBox.information(self, "Empty Field", "Please enter a name and address.") - return - - if self._current_mode == self.AddingMode: - if name not in self.contacts: - self.contacts[name] = address - QMessageBox.information(self, "Add Successful", - f'"{name}" has been added to your address book.') - else: - QMessageBox.information(self, "Add Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - - elif self._current_mode == self.EditingMode: - if self._old_name != name: - if name not in self.contacts: - QMessageBox.information(self, "Edit Successful", - f'"{self.oldName}" has been edited in your ' - 'address book.') - del self.contacts[self._old_name] - self.contacts[name] = address - else: - QMessageBox.information(self, "Edit Unsuccessful", - f'Sorry, "{name}" is already in your address book.') - return - elif self._old_address != address: - QMessageBox.information(self, "Edit Successful", - f'"{name}" has been edited in your address book.') - self.contacts[name] = address - - self.update_interface(self.NavigationMode) - - @Slot() - def cancel(self): - self._name_line.setText(self._old_name) - self._address_text.setText(self._old_address) - self.update_interface(self.NavigationMode) - - @Slot() - def remove_contact(self): - name = self._name_line.text() - - if name in self.contacts: - button = QMessageBox.question(self, "Confirm Remove", - f'Are you sure you want to remove "{name}"?', - QMessageBox.Yes | QMessageBox.No) - - if button == QMessageBox.Yes: - self.previous() - del self.contacts[name] - - QMessageBox.information(self, "Remove Successful", - f'"{name}" has been removed from your address book.') - - self.update_interface(self.NavigationMode) - - @Slot() - def next(self): - name = self._name_line.text() - it = iter(self.contacts) - - try: - while True: - this_name, _ = it.next() - - if this_name == name: - next_name, next_address = it.next() - break - except StopIteration: - next_name, next_address = iter(self.contacts).next() - - self._name_line.setText(next_name) - self._address_text.setText(next_address) - - @Slot() - def previous(self): - name = self._name_line.text() - - prev_name = prev_address = None - for this_name, this_address in self.contacts: - if this_name == name: - break - - prev_name = this_name - prev_address = this_address - else: - self._name_line.clear() - self._address_text.clear() - return - - if prev_name is None: - for prev_name, prev_address in self.contacts: - pass - - self._name_line.setText(prev_name) - self._address_text.setText(prev_address) - - def find_contact(self): - self.dialog.show() - - if self.dialog.exec() == QDialog.Accepted: - contact_name = self.dialog.get_find_text() - - if contact_name in self.contacts: - self._name_line.setText(contact_name) - self._address_text.setText(self.contacts[contact_name]) - else: - QMessageBox.information(self, "Contact Not Found", - f'Sorry, "{contact_name}" is not in your address book.') - return - - self.update_interface(self.NavigationMode) - - def update_interface(self, mode): - self._current_mode = mode - - if self._current_mode in (self.AddingMode, self.EditingMode): - self._name_line.setReadOnly(False) - self._name_line.setFocus(Qt.FocusReason.OtherFocusReason) - self._address_text.setReadOnly(False) - - self._add_button.setEnabled(False) - self._edit_button.setEnabled(False) - self._remove_button.setEnabled(False) - - self._next_button.setEnabled(False) - self._previous_button.setEnabled(False) - - self._submit_button.show() - self._cancel_button.show() - - self._load_button.setEnabled(False) - self._save_button.setEnabled(False) - self._export_button.setEnabled(False) - - elif self._current_mode == self.NavigationMode: - if not self.contacts: - self._name_line.clear() - self._address_text.clear() - - self._name_line.setReadOnly(True) - self._address_text.setReadOnly(True) - self._add_button.setEnabled(True) - - number = len(self.contacts) - self._edit_button.setEnabled(number >= 1) - self._remove_button.setEnabled(number >= 1) - self._find_button.setEnabled(number > 2) - self._next_button.setEnabled(number > 1) - self._previous_button.setEnabled(number > 1) - - self._submit_button.hide() - self._cancel_button.hide() - - self._export_button.setEnabled(number >= 1) - - self._load_button.setEnabled(True) - self._save_button.setEnabled(number >= 1) - - def save_to_file(self): - fileName, _ = QFileDialog.getSaveFileName(self, - "Save Address Book", '', - "Address Book (*.abk);;All Files (*)") - - if not fileName: - return - - try: - out_file = open(str(fileName), 'wb') - except IOError: - QMessageBox.information(self, "Unable to open file", - f'There was an error opening "{fileName}"') - return - - pickle.dump(self.contacts, out_file) - out_file.close() - - def load_from_file(self): - fileName, _ = QFileDialog.getOpenFileName(self, - "Open Address Book", '', - "Address Book (*.abk);;All Files (*)") - - if not fileName: - return - - try: - in_file = open(str(fileName), 'rb') - except IOError: - QMessageBox.information(self, "Unable to open file", - f'There was an error opening "{fileName}"') - return - - self.contacts = pickle.load(in_file) - in_file.close() - - if len(self.contacts) == 0: - QMessageBox.information(self, "No contacts in file", - "The file you are attempting to open contains no contacts.") - else: - for name, address in self.contacts: - self._name_line.setText(name) - self._address_text.setText(address) - - self.update_interface(self.NavigationMode) - - def export_as_vcard(self): - name = str(self._name_line.text()) - address = self._address_text.toPlainText() - - name_list = name.split() - - if len(name_list) > 1: - first_name = name_list[0] - last_name = name_list[-1] - else: - first_name = name - last_name = '' - - file_name = QFileDialog.getSaveFileName(self, "Export Contact", - '', "vCard Files (*.vcf);;All Files (*)")[0] - - if not file_name: - return - - out_file = QFile(file_name) - - if not out_file.open(QIODevice.OpenModeFlag.WriteOnly): - QMessageBox.information(self, "Unable to open file", out_file.errorString()) - return - - out_s = QTextStream(out_file) - - out_s << 'BEGIN:VCARD' << '\n' - out_s << 'VERSION:2.1' << '\n' - out_s << 'N:' << last_name << ';' << first_name << '\n' - out_s << 'FN:' << ' '.join(name_list) << '\n' - - address.replace(';', '\\;') - address.replace('\n', ';') - address.replace(',', ' ') - - out_s << 'ADR;HOME:;' << address << '\n' - out_s << 'END:VCARD' << '\n' - - QMessageBox.information(self, "Export Successful", - f'"{name}" has been exported as a vCard.') - - -class FindDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - - find_label = QLabel("Enter the name of a contact:") - self._line_edit = QLineEdit() - - self._find_button = QPushButton("&Find") - self._find_text = '' - - layout = QHBoxLayout() - layout.addWidget(find_label) - layout.addWidget(self._line_edit) - layout.addWidget(self._find_button) - - self.setLayout(layout) - self.setWindowTitle("Find a Contact") - - self._find_button.clicked.connect(self.find_clicked) - self._find_button.clicked.connect(self.accept) - - def find_clicked(self): - text = self._line_edit.text() - - if not text: - QMessageBox.information(self, "Empty Field", "Please enter a name.") - return - - self._find_text = text - self._line_edit.clear() - self.hide() - - def get_find_text(self): - return self._find_text - - -if __name__ == '__main__': - app = QApplication(sys.argv) - - address_book = AddressBook() - address_book.show() - - sys.exit(app.exec()) diff --git a/examples/widgets/tutorials/cannon/t11.py b/examples/widgets/tutorials/cannon/t11.py index 00ac2fc2..6ba2d1a1 100644 --- a/examples/widgets/tutorials/cannon/t11.py +++ b/examples/widgets/tutorials/cannon/t11.py @@ -9,7 +9,7 @@ import sys import math from PySide6.QtCore import QPoint, QRect, QTimer, Qt, Signal, Slot, qWarning -from PySide6.QtGui import QColor, QFont, QPainter, QPalette, QRegion +from PySide6.QtGui import QColor, QFont, QPainter, QPainterStateGuard, QPalette, QRegion from PySide6.QtWidgets import (QApplication, QGridLayout, QHBoxLayout, QLCDNumber, QPushButton, QSlider, QVBoxLayout, QWidget) @@ -138,12 +138,11 @@ class CannonField(QWidget): painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(Qt.GlobalColor.blue) - painter.save() - painter.translate(0, self.height()) - painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) - painter.rotate(-self._current_angle) - painter.drawRect(CannonField.barrel_rect) - painter.restore() + with QPainterStateGuard(painter): + painter.translate(0, self.height()) + painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) + painter.rotate(-self._current_angle) + painter.drawRect(CannonField.barrel_rect) def cannon_rect(self): result = QRect(0, 0, 50, 50) diff --git a/examples/widgets/tutorials/cannon/t12.py b/examples/widgets/tutorials/cannon/t12.py index 4960797b..c503f9d5 100644 --- a/examples/widgets/tutorials/cannon/t12.py +++ b/examples/widgets/tutorials/cannon/t12.py @@ -10,7 +10,7 @@ import math import random from PySide6.QtCore import QPoint, QRect, QTime, QTimer, Qt, Signal, Slot, qWarning -from PySide6.QtGui import QColor, QFont, QPainter, QPalette, QRegion +from PySide6.QtGui import QColor, QFont, QPainter, QPainterStateGuard, QPalette, QRegion from PySide6.QtWidgets import (QApplication, QGridLayout, QHBoxLayout, QLabel, QLCDNumber, QPushButton, QSlider, QVBoxLayout, QWidget) @@ -184,12 +184,11 @@ class CannonField(QWidget): painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(Qt.GlobalColor.blue) - painter.save() - painter.translate(0, self.height()) - painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) - painter.rotate(-self._current_angle) - painter.drawRect(CannonField.barrel_rect) - painter.restore() + with QPainterStateGuard(painter): + painter.translate(0, self.height()) + painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) + painter.rotate(-self._current_angle) + painter.drawRect(CannonField.barrel_rect) def cannon_rect(self): result = QRect(0, 0, 50, 50) diff --git a/examples/widgets/tutorials/cannon/t13.py b/examples/widgets/tutorials/cannon/t13.py index c9b8bd5d..39912fd1 100644 --- a/examples/widgets/tutorials/cannon/t13.py +++ b/examples/widgets/tutorials/cannon/t13.py @@ -11,7 +11,7 @@ import random from PySide6.QtCore import (QPoint, QRect, QTime, QTimer, Qt, Signal, Slot, qWarning) -from PySide6.QtGui import QColor, QFont, QPainter, QPalette, QRegion +from PySide6.QtGui import QColor, QFont, QPainter, QPainterStateGuard, QPalette, QRegion from PySide6.QtWidgets import (QApplication, QGridLayout, QHBoxLayout, QLabel, QLCDNumber, QPushButton, QSizePolicy, QSlider, QVBoxLayout, QWidget) @@ -211,12 +211,11 @@ class CannonField(QWidget): painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(Qt.GlobalColor.blue) - painter.save() - painter.translate(0, self.height()) - painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) - painter.rotate(-self._current_angle) - painter.drawRect(CannonField.barrel_rect) - painter.restore() + with QPainterStateGuard(painter): + painter.translate(0, self.height()) + painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) + painter.rotate(-self._current_angle) + painter.drawRect(CannonField.barrel_rect) def cannon_rect(self): result = QRect(0, 0, 50, 50) diff --git a/examples/widgets/tutorials/cannon/t14.py b/examples/widgets/tutorials/cannon/t14.py index 00674189..d7824348 100644 --- a/examples/widgets/tutorials/cannon/t14.py +++ b/examples/widgets/tutorials/cannon/t14.py @@ -11,8 +11,8 @@ import random from PySide6.QtCore import (QPoint, QRect, QTime, QTimer, QSize, Qt, Signal, Slot, qWarning) -from PySide6.QtGui import (QColor, QFont, QKeySequence, QPainter, QPalette, - QShortcut, QRegion, QTransform) +from PySide6.QtGui import (QColor, QFont, QKeySequence, QPainter, QPainterStateGuard, + QPalette, QShortcut, QRegion, QTransform) from PySide6.QtWidgets import (QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLCDNumber, QPushButton, QSizePolicy, QSlider, QVBoxLayout, QWidget) @@ -241,12 +241,11 @@ class CannonField(QWidget): painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(Qt.GlobalColor.blue) - painter.save() - painter.translate(0, self.height()) - painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) - painter.rotate(-self._current_angle) - painter.drawRect(CannonField.barrel_rect) - painter.restore() + with QPainterStateGuard(painter): + painter.translate(0, self.height()) + painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) + painter.rotate(-self._current_angle) + painter.drawRect(CannonField.barrel_rect) def cannon_rect(self): result = QRect(0, 0, 50, 50) diff --git a/examples/xml/dombookmarks/dombookmarks.py b/examples/xml/dombookmarks/dombookmarks.py index f5afdeef..4f778acb 100644 --- a/examples/xml/dombookmarks/dombookmarks.py +++ b/examples/xml/dombookmarks/dombookmarks.py @@ -7,7 +7,7 @@ from __future__ import annotations import sys -from PySide6.QtCore import QDir, QFile, Qt, QTextStream +from PySide6.QtCore import QDir, QFile, QObject, Qt, QTextStream from PySide6.QtGui import QAction, QIcon, QKeySequence from PySide6.QtWidgets import (QApplication, QFileDialog, QHeaderView, QMainWindow, QMessageBox, QStyle, QTreeWidget, @@ -93,6 +93,7 @@ class XbelTree(QTreeWidget): def __init__(self, parent=None): super().__init__(parent) + self._update_conn_id = None self.header().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.setHeaderLabels(("Title", "Location")) @@ -131,17 +132,15 @@ class XbelTree(QTreeWidget): self.clear() # It might not be connected. - try: - self.itemChanged.disconnect(self.update_dom_element) - except RuntimeError: - pass + if self._update_conn_id: + QObject.disconnect(self._update_conn_id) child = root.firstChildElement('folder') while not child.isNull(): self.parse_folder_element(child) child = child.nextSiblingElement('folder') - self.itemChanged.connect(self.update_dom_element) + self._update_conn_id = self.itemChanged.connect(self.update_dom_element) return True diff --git a/requirements-coin.txt b/requirements-coin.txt index 5ee2bac0..a5dabc83 100644 --- a/requirements-coin.txt +++ b/requirements-coin.txt @@ -1,5 +1,5 @@ -pip>=24.2 -setuptools==72.1.0 +pip>=25 +setuptools==78.1.0 importlib_metadata>=6 importlib_resources>=5.10.2 packaging>=24 diff --git a/requirements.txt b/requirements.txt index 9769977f..feb5a01a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ # Build dependencies -setuptools==72.1.0 -packaging==24.1 -build==1.2.1 +setuptools==78.1.0 +packaging==24.2 +build==1.2.2.post1 wheel==0.43.0 distro==1.9.0; sys_platform == 'linux' patchelf==0.17.2; sys_platform == 'linux' # 2.0.2 is the last version that supports Python 3.9 numpy<=2.0.2; python_version <= '3.9' numpy==2.1.3; python_version > '3.9' -mypy[faster-cache]>=1.14.0 +mypy>=1.15.0 # note: 3.13 with disable-gil is not compiled yet diff --git a/sources/pyside-tools/deploy.py b/sources/pyside-tools/deploy.py index c26c8c62..b437b6da 100644 --- a/sources/pyside-tools/deploy.py +++ b/sources/pyside-tools/deploy.py @@ -26,7 +26,6 @@ from __future__ import annotations deployment platform etc. Note: This file is used by both pyside6-deploy and pyside6-android-deploy - """ import sys @@ -64,26 +63,27 @@ HELP_MODE = dedent(""" def main(main_file: Path = None, name: str = None, config_file: Path = None, init: bool = False, loglevel=logging.WARNING, dry_run: bool = False, keep_deployment_files: bool = False, force: bool = False, extra_ignore_dirs: str = None, extra_modules_grouped: str = None, - mode: str = None): + mode: str = None) -> str | None: + """ + Entry point for pyside6-deploy command. + + :return: If successful, the Nuitka command that was executed. None otherwise. + """ logging.basicConfig(level=loglevel) - # in case pyside6-deploy is run from a completely different location than the project - # directory + # In case pyside6-deploy is run from a completely different location than the project directory if main_file and main_file.exists(): config_file = main_file.parent / "pysidedeploy.spec" if config_file and not config_file.exists() and not main_file.exists(): raise RuntimeError(dedent(""" Directory does not contain main.py file. - Please specify the main python entrypoint file or the config file. - Run "pyside6-deploy desktop --help" to see info about cli options. + Please specify the main Python entry point file or the pysidedeploy.spec config file. + Run "pyside6-deploy --help" to see info about CLI options. pyside6-deploy exiting...""")) - # Nuitka command to run - command_str = None - config = None logging.info("[DEPLOY] Start") if extra_ignore_dirs: @@ -104,7 +104,7 @@ def main(main_file: Path = None, name: str = None, config_file: Path = None, ini if config_file_exists: logging.info(f"[DEPLOY] Using existing config file {config_file}") else: - config_file = create_config_file(main_file=main_file, dry_run=dry_run, ) + config_file = create_config_file(main_file=main_file, dry_run=dry_run) config = DesktopConfig(config_file=config_file, source_file=main_file, python_exe=python.exe, dry_run=dry_run, existing_config_file=config_file_exists, @@ -130,7 +130,7 @@ def main(main_file: Path = None, name: str = None, config_file: Path = None, ini f"{[str(qml_file) for qml_file in config.qml_files]}") if init: - # config file created above. Exiting. + # Config file created above. Exiting. logging.info(f"[DEPLOY]: Config file {config.config_file} created") return @@ -143,8 +143,9 @@ def main(main_file: Path = None, name: str = None, config_file: Path = None, ini print("[DEPLOY] QtSql Application is not supported on macOS with pyside6-deploy") return + command_str = None try: - # create executable + # Run the Nuitka command to create the executable if not dry_run: logging.info("[DEPLOY] Deploying application") @@ -158,6 +159,8 @@ def main(main_file: Path = None, name: str = None, config_file: Path = None, ini dry_run=dry_run, permissions=config.permissions, mode=config.mode) + if not dry_run: + logging.info("[DEPLOY] Successfully deployed application") except Exception: print(f"[DEPLOY] Exception occurred: {traceback.format_exc()}") finally: diff --git a/sources/pyside-tools/deploy_lib/android/android_utilities.py b/sources/pyside-tools/deploy_lib/android/android_utilities.py index 45dd874f..d70e38ea 100644 --- a/sources/pyside-tools/deploy_lib/android/android_utilities.py +++ b/sources/pyside-tools/deploy_lib/android/android_utilities.py @@ -17,8 +17,8 @@ from tqdm import tqdm # the tag number does not matter much since we update the sdk later DEFAULT_SDK_TAG = 6514223 -ANDROID_NDK_VERSION = "26b" -ANDROID_NDK_VERSION_NUMBER_SUFFIX = "10909125" +ANDROID_NDK_VERSION = "27c" +ANDROID_NDK_VERSION_NUMBER_SUFFIX = "12479018" def run_command(command: list[str], cwd: str | None = None, ignore_fail: bool = False, @@ -163,16 +163,9 @@ def download_android_ndk(ndk_path: Path): print("Unpacking Android Ndk") if sys.platform == "darwin": - extract_dmg(file=(ndk_path - / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"), - destination=ndk_path) - ndk_version_path = (ndk_version_path - / (f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app" - "/Contents/NDK")) + extract_dmg(file=ndk_zip_path, destination=ndk_path) else: - extract_zip(file=(ndk_path - / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"), - destination=ndk_path) + extract_zip(file=ndk_zip_path, destination=ndk_path) except Exception as e: print(f"Error occurred while downloading and unpacking Android NDK: {e}") if ndk_path.exists(): diff --git a/sources/pyside-tools/deploy_lib/config.py b/sources/pyside-tools/deploy_lib/config.py index 998a175c..853f5f6a 100644 --- a/sources/pyside-tools/deploy_lib/config.py +++ b/sources/pyside-tools/deploy_lib/config.py @@ -11,7 +11,7 @@ from configparser import ConfigParser from pathlib import Path from enum import Enum -from project_lib import ProjectData, DesignStudioProject +from project_lib import ProjectData, DesignStudioProject, resolve_valid_project_file from . import (DEFAULT_APP_ICON, DEFAULT_IGNORE_DIRS, find_pyside_modules, find_permission_categories, QtDependencyReader, run_qmlimportscanner) @@ -106,24 +106,21 @@ class Config(BaseConfig): self.extra_ignore_dirs = extra_ignore_dirs self._dry_run = dry_run self.qml_modules = set() - # set source_file + self.source_file = Path( - self.set_or_fetch(config_property_val=source_file, config_property_key="input_file") + self.set_or_fetch(property_value=source_file, property_key="input_file") ).resolve() - # set python path self.python_path = Path( self.set_or_fetch( - config_property_val=python_exe, - config_property_key="python_path", - config_property_group="python", + property_value=python_exe, + property_key="python_path", + property_group="python", ) ) - # set application name - self.title = self.set_or_fetch(config_property_val=name, config_property_key="title") + self.title = self.set_or_fetch(property_value=name, property_key="title") - # set application icon config_icon = self.get_value("app", "icon") if config_icon: self._icon = str(Path(config_icon).resolve()) @@ -176,96 +173,107 @@ class Config(BaseConfig): self.modules = [] - def set_or_fetch(self, config_property_val, config_property_key, config_property_group="app"): + def set_or_fetch(self, property_value, property_key, property_group="app") -> str: """ - Set the configuration value if provided, otherwise fetch the existing value. - Raise an exception if neither is available. + If a new property value is provided, store it in the config file + Otherwise return the existing value in the config file. + Raise an exception if neither are available. - :param value: The value to set if provided. - :param key: The configuration key. - :param group: The configuration group (default is "app"). + :param property_value: The value to set if provided. + :param property_key: The configuration key. + :param property_group: The configuration group (default is "app"). :return: The configuration value. :raises RuntimeError: If no value is provided and no existing value is found. """ - existing_value = self.get_value(config_property_group, config_property_key) + existing_value = self.get_value(property_group, property_key) - if config_property_val: - self.set_value(config_property_group, config_property_key, str(config_property_val)) - return config_property_val - elif existing_value: + if property_value: + self.set_value(property_group, property_key, str(property_value)) + return property_value + if existing_value: return existing_value - else: - raise RuntimeError( - f"[DEPLOY] No value for {config_property_key} specified in config file or as cli" - " option" - ) + + raise RuntimeError( + f"[DEPLOY] No value for {property_key} specified in config file or as cli option" + ) @property - def dry_run(self): + def dry_run(self) -> bool: return self._dry_run @property - def generated_files_path(self): + def generated_files_path(self) -> Path: return self._generated_files_path @property - def qml_files(self): + def qml_files(self) -> list[Path]: return self._qml_files @qml_files.setter - def qml_files(self, qml_files): + def qml_files(self, qml_files: list[Path]): self._qml_files = qml_files qml_files = [str(file.absolute().relative_to(self.project_dir.absolute())) if file.absolute().is_relative_to(self.project_dir) else str(file.absolute()) for file in self.qml_files] + qml_files.sort() self.set_value("qt", "qml_files", ",".join(qml_files)) @property - def project_dir(self): + def project_dir(self) -> Path: return self._project_dir @project_dir.setter - def project_dir(self, project_dir): + def project_dir(self, project_dir: Path) -> None: + rel_path = ( + project_dir.relative_to(self.config_file.parent) + if project_dir.is_relative_to(self.config_file.parent) + else project_dir + ) self._project_dir = project_dir - self.set_value("app", "project_dir", str(project_dir)) + self.set_value("app", "project_dir", str(rel_path)) @property - def project_file(self): + def project_file(self) -> Path: return self._project_file @project_file.setter - def project_file(self, project_file): + def project_file(self, project_file: Path): self._project_file = project_file self.set_value("app", "project_file", str(project_file.relative_to(self.project_dir))) @property - def title(self): + def title(self) -> str: return self._title @title.setter - def title(self, title): + def title(self, title: str): self._title = title @property - def icon(self): + def icon(self) -> str: return self._icon @icon.setter - def icon(self, icon): + def icon(self, icon: str): self._icon = icon self.set_value("app", "icon", icon) @property - def source_file(self): + def source_file(self) -> Path: return self._source_file @source_file.setter - def source_file(self, source_file: Path): + def source_file(self, source_file: Path) -> None: + rel_path = ( + source_file.relative_to(self.config_file.parent) + if source_file.is_relative_to(self.config_file.parent) + else source_file + ) self._source_file = source_file - self.set_value("app", "input_file", str(source_file)) + self.set_value("app", "input_file", str(rel_path)) @property - def python_path(self): + def python_path(self) -> Path: return self._python_path @python_path.setter @@ -273,25 +281,26 @@ class Config(BaseConfig): self._python_path = python_path @property - def extra_args(self): + def extra_args(self) -> str: return self.get_value("nuitka", "extra_args") @extra_args.setter - def extra_args(self, extra_args): + def extra_args(self, extra_args: str): self.set_value("nuitka", "extra_args", extra_args) @property - def excluded_qml_plugins(self): + def excluded_qml_plugins(self) -> list[str]: return self._excluded_qml_plugins @excluded_qml_plugins.setter - def excluded_qml_plugins(self, excluded_qml_plugins): + def excluded_qml_plugins(self, excluded_qml_plugins: list[str]): self._excluded_qml_plugins = excluded_qml_plugins if excluded_qml_plugins: # check required for Android + excluded_qml_plugins.sort() self.set_value("qt", "excluded_qml_plugins", ",".join(excluded_qml_plugins)) @property - def exe_dir(self): + def exe_dir(self) -> Path: return self._exe_dir @exe_dir.setter @@ -300,12 +309,13 @@ class Config(BaseConfig): self.set_value("app", "exec_directory", str(exe_dir)) @property - def modules(self): + def modules(self) -> list[str]: return self._modules @modules.setter - def modules(self, modules): + def modules(self, modules: list[str]): self._modules = modules + modules.sort() self.set_value("qt", "modules", ",".join(modules)) def _find_qml_files(self): @@ -314,7 +324,6 @@ class Config(BaseConfig): field qml_files is empty in the config_file """ - qml_files = [] if self.project_data: qml_files = [(self.project_dir / str(qml_file)) for qml_file in self.project_data.qml_files] @@ -340,7 +349,7 @@ class Config(BaseConfig): if DesignStudioProject.is_ds_project(self.source_file): return DesignStudioProject(self.source_file).project_dir - # there is no other way to find the project_dir than assume it is the parent directory + # There is no other way to find the project_dir than assume it is the parent directory # of source_file return self.source_file.parent @@ -353,15 +362,12 @@ class Config(BaseConfig): else: pyproject_location = self.project_dir - files = list(pyproject_location.glob("*.pyproject")) - if not files: - logging.info("[DEPLOY] No .pyproject file found. Project file not set") - return None - if len(files) > 1: - warnings.warn("DEPLOY: More that one .pyproject files found. Project file not set") - return None - - return files[0] + try: + return resolve_valid_project_file(pyproject_location) + except ValueError as e: + logging.warning(f"[DEPLOY] Unable to resolve a valid project file. Proceeding without a" + f" project file. Details:\n{e}.") + return None def _find_excluded_qml_plugins(self) -> list[str] | None: if not self.qml_files and not DesignStudioProject.is_ds_project(self.source_file): @@ -459,25 +465,27 @@ class DesktopConfig(Config): f"the resources manually using pyside6-rcc") @property - def qt_plugins(self): + def qt_plugins(self) -> list[str]: return self._qt_plugins @qt_plugins.setter - def qt_plugins(self, qt_plugins): + def qt_plugins(self, qt_plugins: list[str]): self._qt_plugins = qt_plugins + qt_plugins.sort() self.set_value("qt", "plugins", ",".join(qt_plugins)) @property - def permissions(self): + def permissions(self) -> list[str]: return self._permissions @permissions.setter - def permissions(self, permissions): + def permissions(self, permissions: list[str]): self._permissions = permissions + permissions.sort() self.set_value("nuitka", "macos.permissions", ",".join(permissions)) @property - def mode(self): + def mode(self) -> NuitkaMode: return self._mode @mode.setter @@ -516,7 +524,7 @@ class DesktopConfig(Config): logging.info(f"[DEPLOY] Usage descriptions for the {perm_categories_str} will be added to " "the Info.plist file of the macOS application bundle") - # handling permissions + # Handling permissions for perm_category in perm_categories: if perm_category in PERMISSION_MAP: permissions.append(PERMISSION_MAP[perm_category]) diff --git a/sources/pyside-tools/deploy_lib/default.spec b/sources/pyside-tools/deploy_lib/default.spec index 618a0294..5e33d539 100644 --- a/sources/pyside-tools/deploy_lib/default.spec +++ b/sources/pyside-tools/deploy_lib/default.spec @@ -3,17 +3,16 @@ # Title of your application title = pyside_app_demo -# Project Directory. The general assumption is that project_dir is the parent directory -# of input_file +# Project root directory. Default: The parent directory of input_file project_dir = -# Source file path +# Source file entry point path. Default: main.py input_file = # Directory where the executable output is generated exec_directory = -# Path to .pyproject project file +# Path to the project file relative to project_dir project_file = # Application icon @@ -24,76 +23,76 @@ icon = # Python path python_path = -# python packages to install -packages = Nuitka==2.5.1 +# Python packages to install +packages = Nuitka==2.7.11 -# buildozer: for deploying Android application +# Buildozer: for deploying Android application android_packages = buildozer==1.5.0,cython==0.29.33 [qt] -# Comma separated path to QML files required -# normally all the QML files required by the project are added automatically +# Paths to required QML files. Comma separated +# Normally all the QML files required by the project are added automatically +# Design Studio projects include the QML files using Qt resources qml_files = -# excluded qml plugin binaries +# Excluded qml plugin binaries excluded_qml_plugins = # Qt modules used. Comma separated modules = -# Qt plugins used by the application. Only relevant for desktop deployment. For Qt plugins used -# in Android application see [android][plugins] +# Qt plugins used by the application. Only relevant for desktop deployment +# For Qt plugins used in Android application see [android][plugins] plugins = [android] -# path to PySide wheel +# Path to PySide wheel wheel_pyside = -# path to Shiboken wheel +# Path to Shiboken wheel wheel_shiboken = -# plugins to be copied to libs folder of the packaged application. Comma separated +# Plugins to be copied to libs folder of the packaged application. Comma separated plugins = [nuitka] -# usage description for permissions requested by the app as found in the Info.plist file -# of the app bundle +# Usage description for permissions requested by the app as found in the Info.plist file +# of the app bundle. Comma separated # eg: NSCameraUsageDescription:CameraAccess macos.permissions = -# mode of using Nuitka. Accepts standalone or onefile. Default is onefile. +# Mode of using Nuitka. Accepts standalone or onefile. Default: onefile mode = onefile -# (str) specify any extra nuitka arguments +# Specify any extra nuitka arguments # eg: extra_args = --show-modules --follow-stdlib extra_args = --quiet --noinclude-qt-translations [buildozer] -# build mode -# possible options: [release, debug] -# release creates an aab, while debug creates an apk +# Build mode +# Possible values: [release, debug] +# Release creates a .aab, while debug creates a .apk mode = debug -# contrains path to PySide6 and shiboken6 recipe dir +# Path to PySide6 and shiboken6 recipe dir recipe_dir = -# path to extra Qt Android jars to be loaded by the application +# Path to extra Qt Android .jar files to be loaded by the application jars_dir = -# if empty uses default ndk path downloaded by buildozer +# If empty, uses default NDK path downloaded by buildozer ndk_path = -# if empty uses default sdk path downloaded by buildozer +# If empty, uses default SDK path downloaded by buildozer sdk_path = -# other libraries to be loaded. Comma separated. -# loaded at app startup +# Other libraries to be loaded at app startup. Comma separated. local_libs = -# architecture of deployed platform -# possible values: ["aarch64", "armv7a", "i686", "x86_64"] +# Architecture of deployed platform +# Possible values: ["aarch64", "armv7a", "i686", "x86_64"] arch = diff --git a/sources/pyside-tools/deploy_lib/dependency_util.py b/sources/pyside-tools/deploy_lib/dependency_util.py index 6bdfb20b..63b40060 100644 --- a/sources/pyside-tools/deploy_lib/dependency_util.py +++ b/sources/pyside-tools/deploy_lib/dependency_util.py @@ -30,15 +30,25 @@ def get_py_files(project_dir: Path, extra_ignore_dirs: tuple[Path] = None, proje qrc_candidates = project_data.qrc_files def add_uic_qrc_candidates(candidates, candidate_type): - possible_py_candidates = [(file.parent / f"{candidate_type}_{file.stem}.py") - for file in candidates - if (file.parent / f"{candidate_type}_{file.stem}.py").exists() - ] - - if len(possible_py_candidates) != len(candidates): - warnings.warn(f"[DEPLOY] The number of {candidate_type} files and their " - "corresponding Python files don't match.", - category=RuntimeWarning) + possible_py_candidates = [] + missing_files = [] + for file in candidates: + py_file = file.parent / f"{candidate_type}_{file.stem}.py" + if py_file.exists(): + possible_py_candidates.append(py_file) + else: + missing_files.append((str(file), str(py_file))) + + if missing_files: + missing_details = "\n".join( + f"{candidate_type.upper()} file: {src} -> Missing Python file: {dst}" + for src, dst in missing_files + ) + warnings.warn( + f"[DEPLOY] The following {candidate_type} files do not have corresponding " + f"Python files:\n {missing_details}", + category=RuntimeWarning + ) py_candidates.extend(possible_py_candidates) @@ -46,7 +56,7 @@ def get_py_files(project_dir: Path, extra_ignore_dirs: tuple[Path] = None, proje add_uic_qrc_candidates(ui_candidates, "ui") if qrc_candidates: - add_uic_qrc_candidates(qrc_candidates, "qrc") + add_uic_qrc_candidates(qrc_candidates, "rc") return py_candidates diff --git a/sources/pyside-tools/metaobjectdump.py b/sources/pyside-tools/metaobjectdump.py index d14c3334..f3c0c560 100644 --- a/sources/pyside-tools/metaobjectdump.py +++ b/sources/pyside-tools/metaobjectdump.py @@ -70,9 +70,11 @@ def _attribute(node: ast.Attribute) -> tuple[str, str]: return node.value.id, node.attr -def _name(node: ast.Name | ast.Attribute) -> str: +def _name(node: ast.Name | ast.Attribute | ast.Constant) -> str: """Return the name of something that is either an attribute or a name, such as base classes or call.func""" + if isinstance(node, ast.Constant): + return str(node.value) if isinstance(node, ast.Attribute): qualifier, name = _attribute(node) return f"{qualifier}.{node.attr}" diff --git a/sources/pyside-tools/project.py b/sources/pyside-tools/project.py index 1af51e07..762e76f3 100644 --- a/sources/pyside-tools/project.py +++ b/sources/pyside-tools/project.py @@ -2,46 +2,36 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only from __future__ import annotations -""" -Builds a '.pyproject' file - -Builds Qt Designer forms, resource files and QML type files - -Deploys the application by creating an executable for the corresponding platform - -For each entry in a '.pyproject' file: -- .pyproject: Recurse to handle subproject -- .qrc : Runs the resource compiler to create a file rc_.py -- .ui : Runs the user interface compiler to create a file ui_.py - -For a Python file declaring a QML module, a directory matching the URI is -created and populated with .qmltypes and qmldir files for use by code analysis -tools. Currently, only one QML module consisting of several classes can be -handled per project file. -""" import sys import os from pathlib import Path from argparse import ArgumentParser, RawTextHelpFormatter -from project_lib import (QmlProjectData, check_qml_decorators, is_python_file, - QMLDIR_FILE, MOD_CMD, METATYPES_JSON_SUFFIX, - SHADER_SUFFIXES, TRANSLATION_SUFFIX, - requires_rebuild, run_command, remove_path, - ProjectData, resolve_project_file, new_project, - ProjectType, ClOptions, DesignStudioProject) - -MODE_HELP = """build Builds the project -run Builds the project and runs the first file") -clean Cleans the build artifacts") -qmllint Runs the qmllint tool -deploy Deploys the application -lupdate Updates translation (.ts) files -new-ui Creates a new QtWidgets project with a Qt Designer-based main window -new-widget Creates a new QtWidgets project with a main window -new-quick Creates a new QtQuick project +from project_lib import (QmlProjectData, check_qml_decorators, is_python_file, migrate_pyproject, + QMLDIR_FILE, MOD_CMD, METATYPES_JSON_SUFFIX, SHADER_SUFFIXES, + TRANSLATION_SUFFIX, requires_rebuild, run_command, remove_path, + ProjectData, resolve_valid_project_file, new_project, NewProjectTypes, + ClOptions, DesignStudioProject) + +DESCRIPTION = """ +pyside6-project is a command line tool for creating, building and deploying Qt for Python +applications. It operates on project files which are also used by Qt Creator. + +Official documentation: +https://doc.qt.io/qtforpython-6/tools/pyside-project.html """ +OPERATION_HELP = { + "build": "Build the project. Compiles resources, UI files, and QML files if existing and " + "necessary.", + "run": "Build and run the project.", + "clean": "Clean build artifacts and generated files from the project directory.", + "qmllint": "Run the qmllint tool on QML files in the project.", + "deploy": "Create a deployable package of the application including all dependencies.", + "lupdate": "Update translation files (.ts) with new strings from source files.", + "migrate-pyproject": "Migrate a *.pyproject file to pyproject.toml format." +} + UIC_CMD = "pyside6-uic" RCC_CMD = "pyside6-rcc" LRELEASE_CMD = "pyside6-lrelease" @@ -51,10 +41,6 @@ QMLLINT_CMD = "pyside6-qmllint" QSB_CMD = "pyside6-qsb" DEPLOY_CMD = "pyside6-deploy" -NEW_PROJECT_TYPES = {"new-quick": ProjectType.QUICK, - "new-ui": ProjectType.WIDGET_FORM, - "new-widget": ProjectType.WIDGET} - def _sort_sources(files: list[Path]) -> list[Path]: """Sort the sources for building, ensure .qrc is last since it might depend @@ -200,11 +186,11 @@ class Project: self._regenerate_qmldir() - def run(self): + def run(self) -> int: """Runs the project""" self.build() cmd = [sys.executable, str(self.project.main_file)] - run_command(cmd, cwd=self.project.project_file.parent) + return run_command(cmd, cwd=self.project.project_file.parent) def _clean_file(self, source: Path): """Clean an artifact.""" @@ -271,33 +257,51 @@ class Project: cmd_prefix = [LUPDATE_CMD] + [os.fspath(p.relative_to(project_dir)) for p in source_files] cmd_prefix.append("-ts") for ts_file in self.project.ts_files: + ts_dir = ts_file.parent + if not ts_dir.exists(): + ts_dir.mkdir(parents=True, exist_ok=True) if requires_rebuild(source_files, ts_file): cmd = cmd_prefix - cmd.append(ts_file.name) + cmd.append(os.fspath(ts_file)) run_command(cmd, cwd=project_dir) -def main(mode: str = None, file: str = None, dry_run: bool = False, quiet: bool = False, - force: bool = False, qml_module: bool = None): +def main(mode: str = None, dry_run: bool = False, quiet: bool = False, force: bool = False, + qml_module: bool = None, project_dir: str = None, project_path: str = None, + legacy_pyproject: bool = False): cl_options = ClOptions(dry_run=dry_run, quiet=quiet, # noqa: F841 force=force, qml_module=qml_module) - new_project_type = NEW_PROJECT_TYPES.get(mode) - if new_project_type: - if not file: - print(f"{mode} requires a directory name.", file=sys.stderr) + if new_project_type := NewProjectTypes.find_by_command(mode): + if not project_dir: + print(f"Error creating new project: {mode} requires a directory name or path", + file=sys.stderr) + sys.exit(1) + + project_dir = Path(project_dir) + try: + project_dir.resolve() + project_dir.mkdir(parents=True, exist_ok=True) + except (OSError, RuntimeError, ValueError): + print("Invalid project name", file=sys.stderr) sys.exit(1) - sys.exit(new_project(file, new_project_type)) - project_file = resolve_project_file(file) - if not project_file: - print(f"Cannot determine project_file {file}", file=sys.stderr) + sys.exit(new_project(project_dir, new_project_type, legacy_pyproject)) + + if mode == "migrate-pyproject": + sys.exit(migrate_pyproject(project_path)) + + try: + project_file = resolve_valid_project_file(project_path) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) sys.exit(1) + project = Project(project_file) if mode == "build": project.build() elif mode == "run": - project.run() + sys.exit(project.run()) elif mode == "clean": project.clean() elif mode == "qmllint": @@ -312,20 +316,33 @@ def main(mode: str = None, file: str = None, dry_run: bool = False, quiet: bool if __name__ == "__main__": - parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) + parser = ArgumentParser(description=DESCRIPTION, formatter_class=RawTextHelpFormatter) parser.add_argument("--quiet", "-q", action="store_true", help="Quiet") parser.add_argument("--dry-run", "-n", action="store_true", help="Only print commands") parser.add_argument("--force", "-f", action="store_true", help="Force rebuild") parser.add_argument("--qml-module", "-Q", action="store_true", help="Perform check for QML module") - mode_choices = ["build", "run", "clean", "qmllint", "deploy", "lupdate"] - mode_choices.extend(NEW_PROJECT_TYPES.keys()) - parser.add_argument("mode", choices=mode_choices, default="build", - type=str, help=MODE_HELP) - # TODO: improve the command structure. - # "File" argument is not correct when doing new-... project - parser.add_argument("file", help="Project file", nargs="?", type=str) + # Create subparsers for the two different command branches + subparsers = parser.add_subparsers(dest='mode', required=True) + + # Add subparser for project creation commands + for project_type in NewProjectTypes: + new_parser = subparsers.add_parser(project_type.value.command, + help=project_type.value.description) + new_parser.add_argument( + "project_dir", help="Name or location of the new project", nargs="?", type=str) + + new_parser.add_argument( + "--legacy-pyproject", action="store_true", help="Create a legacy *.pyproject file") + + # Add subparser for project operation commands + for op_mode, op_help in OPERATION_HELP.items(): + op_parser = subparsers.add_parser(op_mode, help=op_help) + op_parser.add_argument("project_path", nargs="?", type=str, help="Path to the project file") args = parser.parse_args() - main(args.mode, args.file, args.dry_run, args.quiet, args.force, args.qml_module) + + main(args.mode, args.dry_run, args.quiet, args.force, args.qml_module, + getattr(args, "project_dir", None), getattr(args, "project_path", None), + getattr(args, "legacy_pyproject", None)) diff --git a/sources/pyside-tools/project_lib/__init__.py b/sources/pyside-tools/project_lib/__init__.py index aafaa44b..3c6aa502 100644 --- a/sources/pyside-tools/project_lib/__init__.py +++ b/sources/pyside-tools/project_lib/__init__.py @@ -7,7 +7,10 @@ from dataclasses import dataclass QTPATHS_CMD = "qtpaths6" MOD_CMD = "pyside6-metaobjectdump" -PROJECT_FILE_SUFFIX = ".pyproject" +PYPROJECT_TOML_PATTERN = "pyproject.toml" +PYPROJECT_JSON_PATTERN = "*.pyproject" +# Note that the order is important, as the first pattern that matches is used +PYPROJECT_FILE_PATTERNS = [PYPROJECT_TOML_PATTERN, PYPROJECT_JSON_PATTERN] QMLDIR_FILE = "qmldir" QML_IMPORT_NAME = "QML_IMPORT_NAME" @@ -41,8 +44,10 @@ class ClOptions(metaclass=Singleton): from .utils import (run_command, requires_rebuild, remove_path, package_dir, qtpaths, - qt_metatype_json_dir, resolve_project_file) + qt_metatype_json_dir, resolve_valid_project_file) from .project_data import (is_python_file, ProjectData, QmlProjectData, check_qml_decorators) -from .newproject import new_project, ProjectType +from .newproject import new_project, NewProjectTypes from .design_studio_project import DesignStudioProject +from .pyproject_toml import parse_pyproject_toml, write_pyproject_toml, migrate_pyproject +from .pyproject_json import parse_pyproject_json diff --git a/sources/pyside-tools/project_lib/newproject.py b/sources/pyside-tools/project_lib/newproject.py index 80ed5a75..d8331873 100644 --- a/sources/pyside-tools/project_lib/newproject.py +++ b/sources/pyside-tools/project_lib/newproject.py @@ -2,23 +2,16 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only from __future__ import annotations -import json import os import sys +from dataclasses import dataclass from enum import Enum from pathlib import Path -"""New project generation code.""" - - -Project = list[tuple[str, str]] # tuple of (filename, contents). - - -class ProjectType(Enum): - WIDGET_FORM = 1 - WIDGET = 2 - QUICK = 3 +from .pyproject_toml import write_pyproject_toml +from .pyproject_json import write_pyproject_json +"""New project generation code.""" _WIDGET_MAIN = """if __name__ == '__main__': app = QApplication(sys.argv) @@ -27,23 +20,19 @@ _WIDGET_MAIN = """if __name__ == '__main__': sys.exit(app.exec()) """ - _WIDGET_IMPORTS = """import sys from PySide6.QtWidgets import QApplication, QMainWindow """ - _WIDGET_CLASS_DEFINITION = """class MainWindow(QMainWindow): def __init__(self): super().__init__() """ - _WIDGET_SETUP_UI_CODE = """ self._ui = Ui_MainWindow() self._ui.setupUi(self) """ - _MAINWINDOW_FORM = """ MainWindow @@ -75,7 +64,6 @@ _MAINWINDOW_FORM = """ """ - _QUICK_FORM = """import QtQuick import QtQuick.Controls @@ -107,28 +95,46 @@ if __name__ == "__main__": sys.exit(exit_code) """ +NewProjectFiles = list[tuple[str, str]] # tuple of (filename, contents). + -def _write_project(directory: Path, files: Project): - """Write out the project.""" - file_list = [] - for file, contents in files: - (directory / file).write_text(contents) - print(f"Wrote {directory.name}{os.sep}{file}.") - file_list.append(file) - pyproject = {"files": file_list} - pyproject_file = f"{directory}.pyproject" - (directory / pyproject_file).write_text(json.dumps(pyproject)) - print(f"Wrote {directory.name}{os.sep}{pyproject_file}.") +@dataclass(frozen=True) +class NewProjectType: + command: str + description: str + files: NewProjectFiles -def _widget_project() -> Project: +def _write_project(directory: Path, files: NewProjectFiles, legacy_pyproject: bool): + """ + Create the project files in the specified directory. + + :param directory: The directory to create the project in. + :param files: The files that belong to the project to create. + """ + file_names = [] + for file_name, contents in files: + (directory / file_name).write_text(contents) + print(f"Wrote {directory.name}{os.sep}{file_name}.") + file_names.append(file_name) + + if legacy_pyproject: + pyproject_file = directory / f"{directory.name}.pyproject" + write_pyproject_json(pyproject_file, file_names) + else: + pyproject_file = directory / "pyproject.toml" + write_pyproject_toml(pyproject_file, directory.name, file_names) + print(f"Wrote {pyproject_file}.") + + +def _widget_project() -> NewProjectFiles: """Create a (form-less) widgets project.""" main_py = (_WIDGET_IMPORTS + "\n\n" + _WIDGET_CLASS_DEFINITION + "\n\n" + _WIDGET_MAIN) return [("main.py", main_py)] -def _ui_form_project() -> Project: +def _ui_form_project() -> NewProjectFiles: """Create a Qt Designer .ui form based widgets project.""" main_py = (_WIDGET_IMPORTS + "\nfrom ui_mainwindow import Ui_MainWindow\n\n\n" @@ -138,28 +144,46 @@ def _ui_form_project() -> Project: ("mainwindow.ui", _MAINWINDOW_FORM)] -def _qml_project() -> Project: +def _qml_project() -> NewProjectFiles: """Create a QML project.""" return [("main.py", _QUICK_MAIN), ("main.qml", _QUICK_FORM)] -def new_project(directory_s: str, - project_type: ProjectType = ProjectType.WIDGET_FORM) -> int: - directory = Path(directory_s) - if directory.exists(): - print(f"{directory_s} already exists.", file=sys.stderr) - return -1 - directory.mkdir(parents=True) +class NewProjectTypes(Enum): + QUICK = NewProjectType("new-quick", "Create a new Qt Quick project", _qml_project()) + WIDGET_FORM = NewProjectType("new-ui", "Create a new Qt Widgets Form project", + _ui_form_project()) + WIDGET = NewProjectType("new-widget", "Create a new Qt Widgets project", _widget_project()) - if project_type == ProjectType.WIDGET_FORM: - project = _ui_form_project() - elif project_type == ProjectType.QUICK: - project = _qml_project() - else: - project = _widget_project() - _write_project(directory, project) - if project_type == ProjectType.WIDGET_FORM: - print(f'Run "pyside6-project build {directory_s}" to build the project') - print(f'Run "python {directory.name}{os.sep}main.py" to run the project') + @staticmethod + def find_by_command(command: str) -> NewProjectType | None: + return next((pt.value for pt in NewProjectTypes if pt.value.command == command), None) + + +def new_project( + project_dir: Path, project_type: NewProjectType, legacy_pyproject: bool +) -> int: + """ + Create a new project at the specified project_dir directory. + + :param project_dir: The directory path to create the project. If existing, must be empty. + :param project_type: The Qt type of project to create (Qt Widgets, Qt Quick, etc.) + + :return: 0 if the project was created successfully, otherwise 1. + """ + if any(project_dir.iterdir()): + print(f"Can not create project at {project_dir}: directory is not empty.", file=sys.stderr) + return 1 + project_dir.mkdir(parents=True, exist_ok=True) + + try: + _write_project(project_dir, project_type.files, legacy_pyproject) + except Exception as e: + print(f"Error creating project file: {str(e)}", file=sys.stderr) + return 1 + + if project_type == NewProjectTypes.WIDGET_FORM: + print(f'Run "pyside6-project build {project_dir}" to build the project') + print(f'Run "pyside6-project run {project_dir / "main.py"}" to run the project') return 0 diff --git a/sources/pyside-tools/project_lib/project_data.py b/sources/pyside-tools/project_lib/project_data.py index 445f02b8..9a219c95 100644 --- a/sources/pyside-tools/project_lib/project_data.py +++ b/sources/pyside-tools/project_lib/project_data.py @@ -7,9 +7,11 @@ import os import subprocess import sys from pathlib import Path -from . import (METATYPES_JSON_SUFFIX, PROJECT_FILE_SUFFIX, TRANSLATION_SUFFIX, - qt_metatype_json_dir, MOD_CMD, QML_IMPORT_MAJOR_VERSION, - QML_IMPORT_MINOR_VERSION, QML_IMPORT_NAME, QT_MODULES) +from . import (METATYPES_JSON_SUFFIX, PYPROJECT_JSON_PATTERN, PYPROJECT_TOML_PATTERN, + PYPROJECT_FILE_PATTERNS, TRANSLATION_SUFFIX, qt_metatype_json_dir, MOD_CMD, + QML_IMPORT_MAJOR_VERSION, QML_IMPORT_MINOR_VERSION, QML_IMPORT_NAME, QT_MODULES) +from .pyproject_toml import parse_pyproject_toml +from .pyproject_json import parse_pyproject_json def is_python_file(file: Path) -> bool: @@ -19,7 +21,7 @@ def is_python_file(file: Path) -> bool: class ProjectData: def __init__(self, project_file: Path) -> None: - """Parse the project.""" + """Parse the project file.""" self._project_file = project_file.resolve() self._sub_projects_files: list[Path] = [] @@ -37,26 +39,39 @@ class ProjectData: # ts files self._ts_files: list[Path] = [] - with project_file.open("r") as pyf: - pyproject = json.load(pyf) - for f in pyproject["files"]: - file = Path(project_file.parent / f) - if file.suffix == PROJECT_FILE_SUFFIX: - self._sub_projects_files.append(file) - else: - self._files.append(file) - if file.suffix == ".qml": - self._qml_files.append(file) - elif is_python_file(file): - if file.stem == "main": - self.main_file = file - self._python_files.append(file) - elif file.suffix == ".ui": - self._ui_files.append(file) - elif file.suffix == ".qrc": - self._qrc_files.append(file) - elif file.suffix == TRANSLATION_SUFFIX: - self._ts_files.append(file) + if project_file.match(PYPROJECT_JSON_PATTERN): + project_file_data = parse_pyproject_json(project_file) + elif project_file.match(PYPROJECT_TOML_PATTERN): + project_file_data = parse_pyproject_toml(project_file) + else: + print(f"Unknown project file format: {project_file}", file=sys.stderr) + sys.exit(1) + + if project_file_data.errors: + print(f"Invalid project file: {project_file}. Errors found:", file=sys.stderr) + for error in project_file_data.errors: + print(f"{error}", file=sys.stderr) + sys.exit(1) + + for f in project_file_data.files: + file = Path(project_file.parent / f) + if any(file.match(pattern) for pattern in PYPROJECT_FILE_PATTERNS): + self._sub_projects_files.append(file) + continue + + self._files.append(file) + if file.suffix == ".qml": + self._qml_files.append(file) + elif is_python_file(file): + if file.stem == "main": + self.main_file = file + self._python_files.append(file) + elif file.suffix == ".ui": + self._ui_files.append(file) + elif file.suffix == ".qrc": + self._qrc_files.append(file) + elif file.suffix == TRANSLATION_SUFFIX: + self._ts_files.append(file) if not self.main_file: self._find_main_file() diff --git a/sources/pyside-tools/project_lib/pyproject_json.py b/sources/pyside-tools/project_lib/pyproject_json.py new file mode 100644 index 00000000..2b0b9da1 --- /dev/null +++ b/sources/pyside-tools/project_lib/pyproject_json.py @@ -0,0 +1,58 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +import json +from pathlib import Path + +from .pyproject_parse_result import PyProjectParseResult + + +def write_pyproject_json(pyproject_file: Path, project_files: list[str]): + """ + Create or update a *.pyproject file with the specified content. + + :param pyproject_file: The *.pyproject file path to create or update. + :param project_files: The relative paths of the files to include in the project. + """ + # The content of the file is fully replaced, so it is not necessary to read and merge any + # existing content + content = { + "files": sorted(project_files), + } + pyproject_file.write_text(json.dumps(content), encoding="utf-8") + + +def parse_pyproject_json(pyproject_json_file: Path) -> PyProjectParseResult: + """ + Parse a pyproject.json file and return a PyProjectParseResult object. + """ + result = PyProjectParseResult() + try: + with pyproject_json_file.open("r") as pyf: + project_file_data = json.load(pyf) + except json.JSONDecodeError as e: + result.errors.append(str(e)) + return result + except Exception as e: + result.errors.append(str(e)) + return result + + if not isinstance(project_file_data, dict): + result.errors.append("The root element of pyproject.json must be a JSON object") + return result + + found_files = project_file_data.get("files") + if found_files and not isinstance(found_files, list): + result.errors.append("The files element must be a list") + return result + + for file in project_file_data.get("files", []): + if not isinstance(file, str): + result.errors.append(f"Invalid file: {file}") + return result + + file_path = Path(file) + if not file_path.is_absolute(): + file_path = (pyproject_json_file.parent / file).resolve() + result.files.append(file_path) + + return result diff --git a/sources/pyside-tools/project_lib/pyproject_parse_result.py b/sources/pyside-tools/project_lib/pyproject_parse_result.py new file mode 100644 index 00000000..6a04bf5c --- /dev/null +++ b/sources/pyside-tools/project_lib/pyproject_parse_result.py @@ -0,0 +1,10 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +from dataclasses import dataclass, field +from pathlib import Path + + +@dataclass +class PyProjectParseResult: + errors: list[str] = field(default_factory=list) + files: list[Path] = field(default_factory=list) diff --git a/sources/pyside-tools/project_lib/pyproject_toml.py b/sources/pyside-tools/project_lib/pyproject_toml.py new file mode 100644 index 00000000..fafe0d67 --- /dev/null +++ b/sources/pyside-tools/project_lib/pyproject_toml.py @@ -0,0 +1,235 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +from __future__ import annotations + +import sys +# TODO: Remove this import when Python 3.11 is the minimum supported version +if sys.version_info >= (3, 11): + import tomllib +from pathlib import Path + +from . import PYPROJECT_JSON_PATTERN +from .pyproject_parse_result import PyProjectParseResult +from .pyproject_json import parse_pyproject_json + + +def _parse_toml_content(content: str) -> dict: + """ + Parse TOML content for project name and files list only. + """ + result = {"project": {}, "tool": {"pyside6-project": {}}} + current_section = None + + for line in content.splitlines(): + line = line.strip() + if not line or line.startswith('#'): + continue + + if line == '[project]': + current_section = 'project' + elif line == '[tool.pyside6-project]': + current_section = 'tool.pyside6-project' + elif '=' in line and current_section: + key, value = [part.strip() for part in line.split('=', 1)] + + # Handle string values - name of the project + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + # Handle array of strings - files names + elif value.startswith('[') and value.endswith(']'): + items = value[1:-1].split(',') + value = [item.strip().strip('"') for item in items if item.strip()] + + if current_section == 'project': + result['project'][key] = value + else: # tool.pyside6-project + result['tool']['pyside6-project'][key] = value + + return result + + +def _write_toml_content(data: dict) -> str: + """ + Write minimal TOML content with project and tool.pyside6-project sections. + """ + lines = [] + + if 'project' in data and data['project']: + lines.append('[project]') + for key, value in sorted(data['project'].items()): + if isinstance(value, str): + lines.append(f'{key} = "{value}"') + + if 'tool' in data and 'pyside6-project' in data['tool']: + lines.append('\n[tool.pyside6-project]') + for key, value in sorted(data['tool']['pyside6-project'].items()): + if isinstance(value, list): + items = [f'"{item}"' for item in sorted(value)] + lines.append(f'{key} = [{", ".join(items)}]') + else: + lines.append(f'{key} = "{value}"') + + return '\n'.join(lines) + + +def parse_pyproject_toml(pyproject_toml_file: Path) -> PyProjectParseResult: + """ + Parse a pyproject.toml file and return a PyProjectParseResult object. + """ + result = PyProjectParseResult() + + try: + content = pyproject_toml_file.read_text(encoding='utf-8') + # TODO: Remove the manual parsing when Python 3.11 is the minimum supported version + if sys.version_info >= (3, 11): + root_table = tomllib.loads(content) # Use tomllib for Python >= 3.11 + print("Using tomllib for parsing TOML content") + else: + root_table = _parse_toml_content(content) # Fallback to manual parsing + except Exception as e: + result.errors.append(str(e)) + return result + + pyside_table = root_table.get("tool", {}).get("pyside6-project", {}) + if not pyside_table: + result.errors.append("Missing [tool.pyside6-project] table") + return result + + files = pyside_table.get("files", []) + if not isinstance(files, list): + result.errors.append("Missing or invalid files list") + return result + + # Convert paths + for file in files: + if not isinstance(file, str): + result.errors.append(f"Invalid file: {file}") + return result + file_path = Path(file) + if not file_path.is_absolute(): + file_path = (pyproject_toml_file.parent / file).resolve() + result.files.append(file_path) + + return result + + +def write_pyproject_toml(pyproject_file: Path, project_name: str, project_files: list[str]): + """ + Create or update a pyproject.toml file with the specified content. + """ + data = { + "project": {"name": project_name}, + "tool": { + "pyside6-project": {"files": sorted(project_files)} + } + } + + try: + content = _write_toml_content(data) + pyproject_file.write_text(content, encoding='utf-8') + except Exception as e: + raise ValueError(f"Error writing TOML file: {str(e)}") + + +def migrate_pyproject(pyproject_file: Path | str = None) -> int: + """ + Migrate a project *.pyproject JSON file to the new pyproject.toml format. + + The containing subprojects are migrated recursively. + + :return: 0 if successful, 1 if an error occurred. + """ + project_name = None + + # Transform the user input string into a Path object + if isinstance(pyproject_file, str): + pyproject_file = Path(pyproject_file) + + if pyproject_file: + if not pyproject_file.match(PYPROJECT_JSON_PATTERN): + print(f"Cannot migrate non \"{PYPROJECT_JSON_PATTERN}\" file:", file=sys.stderr) + print(f"\"{pyproject_file}\"", file=sys.stderr) + return 1 + project_files = [pyproject_file] + project_name = pyproject_file.stem + else: + # Get the existing *.pyproject files in the current directory + project_files = list(Path().glob(PYPROJECT_JSON_PATTERN)) + if not project_files: + print(f"No project file found in the current directory: {Path()}", file=sys.stderr) + return 1 + if len(project_files) > 1: + print("Multiple pyproject files found in the project folder:") + print('\n'.join(str(project_file) for project_file in project_files)) + response = input("Continue? y/n: ") + if response.lower().strip() not in {"yes", "y"}: + return 0 + else: + # If there is only one *.pyproject file in the current directory, + # use its file name as the project name + project_name = project_files[0].stem + + # The project files that will be written to the pyproject.toml file + output_files = set() + for project_file in project_files: + project_data = parse_pyproject_json(project_file) + if project_data.errors: + print(f"Invalid project file: {project_file}. Errors found:", file=sys.stderr) + print('\n'.join(project_data.errors), file=sys.stderr) + return 1 + output_files.update(project_data.files) + + project_folder = project_files[0].parent.resolve() + if project_name is None: + # If a project name has not resolved, use the name of the parent folder + project_name = project_folder.name + + pyproject_toml_file = project_folder / "pyproject.toml" + if pyproject_toml_file.exists(): + already_existing_file = True + try: + content = pyproject_toml_file.read_text(encoding='utf-8') + data = _parse_toml_content(content) + except Exception as e: + raise ValueError(f"Error parsing TOML: {str(e)}") + else: + already_existing_file = False + data = {"project": {}, "tool": {"pyside6-project": {}}} + + # Update project name if not present + if "name" not in data["project"]: + data["project"]["name"] = project_name + + # Update files list + data["tool"]["pyside6-project"]["files"] = sorted( + p.relative_to(project_folder).as_posix() for p in output_files + ) + + # Generate TOML content + toml_content = _write_toml_content(data) + + if already_existing_file: + print(f"WARNING: A pyproject.toml file already exists at \"{pyproject_toml_file}\"") + print("The file will be updated with the following content:") + print(toml_content) + response = input("Proceed? [Y/n] ") + if response.lower().strip() not in {"yes", "y"}: + return 0 + + try: + pyproject_toml_file.write_text(toml_content) + except Exception as e: + print(f"Error writing to \"{pyproject_toml_file}\": {str(e)}", file=sys.stderr) + return 1 + + if not already_existing_file: + print(f"Created \"{pyproject_toml_file}\"") + else: + print(f"Updated \"{pyproject_toml_file}\"") + + # Recursively migrate the subprojects + for sub_project_file in filter(lambda f: f.match(PYPROJECT_JSON_PATTERN), output_files): + result = migrate_pyproject(sub_project_file) + if result != 0: + return result + return 0 diff --git a/sources/pyside-tools/project_lib/utils.py b/sources/pyside-tools/project_lib/utils.py index f1e3f0c0..c1c40650 100644 --- a/sources/pyside-tools/project_lib/utils.py +++ b/sources/pyside-tools/project_lib/utils.py @@ -2,23 +2,38 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only from __future__ import annotations -import sys import subprocess -from pathlib import Path +import sys import xml.etree.ElementTree as ET +from pathlib import Path + +from . import (QTPATHS_CMD, PYPROJECT_JSON_PATTERN, PYPROJECT_TOML_PATTERN, PYPROJECT_FILE_PATTERNS, + ClOptions) +from .pyproject_toml import parse_pyproject_toml +from .pyproject_json import parse_pyproject_json + -from . import QTPATHS_CMD, PROJECT_FILE_SUFFIX, ClOptions +def run_command(command: list[str], cwd: str = None, ignore_fail: bool = False) -> int: + """ + Run a command using a subprocess. + If dry run is enabled, the command will be printed to stdout instead of being executed. + :param command: The command to run including the arguments + :param cwd: The working directory to run the command in + :param ignore_fail: If True, the current process will not exit if the command fails -def run_command(command: list[str], cwd: str = None, ignore_fail: bool = False): - """Run a command observing quiet/dry run""" + :return: The exit code of the command + """ cloptions = ClOptions() if not cloptions.quiet or cloptions.dry_run: print(" ".join(command)) - if not cloptions.dry_run: - ex = subprocess.call(command, cwd=cwd) - if ex != 0 and not ignore_fail: - sys.exit(ex) + if cloptions.dry_run: + return 0 + + ex = subprocess.call(command, cwd=cwd) + if ex != 0 and not ignore_fail: + sys.exit(ex) + return ex def qrc_file_requires_rebuild(resources_file_path: Path, compiled_resources_path: Path) -> bool: @@ -46,7 +61,7 @@ def requires_rebuild(sources: list[Path], artifact: Path) -> bool: if source.stat().st_mtime > artifact_mod_time: return True # The .qrc file references other files that might have changed - if source.suffix == '.qrc' and qrc_file_requires_rebuild(source, artifact): + if source.suffix == ".qrc" and qrc_file_requires_rebuild(source, artifact): return True return False @@ -115,13 +130,65 @@ def qt_metatype_json_dir() -> Path: return _qt_metatype_json_dir -def resolve_project_file(cmdline: str) -> Path | None: - """Return the project file from the command line value, either - from the file argument or directory""" - project_file = Path(cmdline).resolve() if cmdline else Path.cwd() - if project_file.is_file(): +def resolve_valid_project_file( + project_path_input: str = None, project_file_patterns: list[str] = PYPROJECT_FILE_PATTERNS +) -> Path: + """ + Find a valid project file given a preferred project file name and a list of project file name + patterns for a fallback search. + + If the provided file name is a valid project file, return it. Otherwise, search for a known + project file in the current working directory with the given patterns. + + Raises a ValueError if no project file is found, multiple project files are found in the same + directory or the provided path is not a valid project file or folder. + + :param project_path_input: The command-line argument specifying a project file or folder path. + :param project_file_patterns: The list of project file patterns to search for. + + :return: The resolved project file path + """ + if project_path_input and (project_file := Path(project_path_input).resolve()).is_file(): + if project_file.match(PYPROJECT_TOML_PATTERN): + if bool(parse_pyproject_toml(project_file).errors): + raise ValueError(f"Invalid project file: {project_file}") + elif project_file.match(PYPROJECT_JSON_PATTERN): + pyproject_json_result = parse_pyproject_json(project_file) + if errors := '\n'.join(str(e) for e in pyproject_json_result.errors): + raise ValueError(f"Invalid project file: {project_file}\n{errors}") + else: + raise ValueError(f"Unknown project file: {project_file}") + return project_file + + project_folder = Path.cwd() + if project_path_input: + if not Path(project_path_input).resolve().is_dir(): + raise ValueError(f"Invalid project path: {project_path_input}") + project_folder = Path(project_path_input).resolve() + + # Search a project file in the project folder using the provided patterns + for pattern in project_file_patterns: + if not (matches := list(project_folder.glob(pattern))): + # No project files found with the specified pattern + continue + + if len(matches) > 1: + matched_files = '\n'.join(str(f) for f in matches) + raise ValueError(f"Multiple project files found:\n{matched_files}") + + project_file = matches[0] + + if pattern == PYPROJECT_TOML_PATTERN: + if parse_pyproject_toml(project_file).errors: + # Invalid file, but a .pyproject file may exist + # We can not raise an error due to ensuring backward compatibility + continue + elif pattern == PYPROJECT_JSON_PATTERN: + pyproject_json_result = parse_pyproject_json(project_file) + if errors := '\n'.join(str(e) for e in pyproject_json_result.errors): + raise ValueError(f"Invalid project file: {project_file}\n{errors}") + + # Found a valid project file return project_file - if project_file.is_dir(): - for m in project_file.glob(f"*{PROJECT_FILE_SUFFIX}"): - return m - return None + + raise ValueError("No project file found in the current directory") diff --git a/sources/pyside-tools/pyside_tool.py b/sources/pyside-tools/pyside_tool.py index 887f2bdd..5c009859 100644 --- a/sources/pyside-tools/pyside_tool.py +++ b/sources/pyside-tools/pyside_tool.py @@ -198,6 +198,18 @@ def metaobjectdump(): pyside_script_wrapper("metaobjectdump.py") +def _check_requirements(requirements_file): + """Check if all required packages are installed.""" + missing_packages = [] + with open(requirements_file, 'r', encoding='UTF-8') as file: + for line in file: + # versions + package = line.strip().split('==')[0] + if not importlib.util.find_spec(package): + missing_packages.append(line.strip()) + return missing_packages + + def project(): pyside_script_wrapper("project.py") @@ -220,12 +232,15 @@ def android_deploy(): file=sys.stderr) else: android_requirements_file = Path(__file__).parent / "requirements-android.txt" - with open(android_requirements_file, 'r', encoding='UTF-8') as file: - while line := file.readline(): - dependent_package = line.rstrip() - if not bool(importlib.util.find_spec(dependent_package)): - command = [sys.executable, "-m", "pip", "install", dependent_package] - subprocess.run(command) + if android_requirements_file.exists(): + missing_packages = _check_requirements(android_requirements_file) + if missing_packages: + print("The following packages are required but not installed:") + for package in missing_packages: + print(f" - {package}") + print("Please install them using:") + print(f" pip install -r {android_requirements_file}") + sys.exit(1) pyside_script_wrapper("android_deploy.py") diff --git a/sources/pyside-tools/requirements-android.txt b/sources/pyside-tools/requirements-android.txt index 9ed5d842..1a247f6c 100644 --- a/sources/pyside-tools/requirements-android.txt +++ b/sources/pyside-tools/requirements-android.txt @@ -1,3 +1,4 @@ jinja2 pkginfo tqdm +packaging==24.1 diff --git a/sources/pyside6/.cmake.conf b/sources/pyside6/.cmake.conf index 86ca5ff4..f1458e93 100644 --- a/sources/pyside6/.cmake.conf +++ b/sources/pyside6/.cmake.conf @@ -1,5 +1,5 @@ set(pyside_MAJOR_VERSION "6") -set(pyside_MINOR_VERSION "8") -set(pyside_MICRO_VERSION "2.1") +set(pyside_MINOR_VERSION "9") +set(pyside_MICRO_VERSION "2") set(pyside_PRE_RELEASE_VERSION_TYPE "") set(pyside_PRE_RELEASE_VERSION "") diff --git a/sources/pyside6/CMakeLists.txt b/sources/pyside6/CMakeLists.txt index f45c0711..9c238e98 100644 --- a/sources/pyside6/CMakeLists.txt +++ b/sources/pyside6/CMakeLists.txt @@ -25,6 +25,10 @@ if(Qt${QT_MAJOR_VERSION}Qml_FOUND) add_subdirectory(libpysideqml) endif() +if(Qt${QT_MAJOR_VERSION}RemoteObjects_FOUND) + add_subdirectory(libpysideremoteobjects) +endif() + if(Qt${QT_MAJOR_VERSION}UiTools_FOUND) add_subdirectory(plugins/uitools) find_package(Qt6 COMPONENTS Designer) diff --git a/sources/pyside6/PySide6/CMakeLists.txt b/sources/pyside6/PySide6/CMakeLists.txt index 37b7a6c9..e5b0b672 100644 --- a/sources/pyside6/PySide6/CMakeLists.txt +++ b/sources/pyside6/PySide6/CMakeLists.txt @@ -105,8 +105,6 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/templates/widgets_common.xml DESTINATION share/PySide6${pyside_SUFFIX}/typesystems) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/templates/datavisualization_common.xml DESTINATION share/PySide6${pyside_SUFFIX}/typesystems) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/templates/opengl_common.xml - DESTINATION share/PySide6${pyside_SUFFIX}/typesystems) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pyside6_global.h DESTINATION include/${BINDING_NAME}${pyside6_SUFFIX}) diff --git a/sources/pyside6/PySide6/Qt3DAnimation/typesystem_3danimation.xml b/sources/pyside6/PySide6/Qt3DAnimation/typesystem_3danimation.xml index c74f6467..88dd4354 100644 --- a/sources/pyside6/PySide6/Qt3DAnimation/typesystem_3danimation.xml +++ b/sources/pyside6/PySide6/Qt3DAnimation/typesystem_3danimation.xml @@ -4,7 +4,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only --> - diff --git a/sources/pyside6/PySide6/QtAsyncio/events.py b/sources/pyside6/PySide6/QtAsyncio/events.py index 36c7fea9..65f3ccbc 100644 --- a/sources/pyside6/PySide6/QtAsyncio/events.py +++ b/sources/pyside6/PySide6/QtAsyncio/events.py @@ -598,7 +598,9 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): def default_exception_handler(self, context: dict[str, Any]) -> None: # TODO if context["message"]: - print(context["message"]) + print(f"{context['message']} from task {context['task']._name}," + "read the following traceback:") + print(context["traceback"]) def call_exception_handler(self, context: dict[str, Any]) -> None: if self._exception_handler is not None: diff --git a/sources/pyside6/PySide6/QtAsyncio/tasks.py b/sources/pyside6/PySide6/QtAsyncio/tasks.py index be1809d5..deabf690 100644 --- a/sources/pyside6/PySide6/QtAsyncio/tasks.py +++ b/sources/pyside6/PySide6/QtAsyncio/tasks.py @@ -4,6 +4,7 @@ from __future__ import annotations from . import events from . import futures +import traceback from typing import Any @@ -38,6 +39,8 @@ class QAsyncioTask(futures.QAsyncioFuture): self._cancelled = False # PYSIDE-2644; see _step self._cancel_count = 0 self._cancel_message: str | None = None + # Store traceback in case of Exception. Useful when exception happens in coroutine + self._tb: str = None # https://docs.python.org/3/library/asyncio-extending.html#task-lifetime-support asyncio._register_task(self) # type: ignore[arg-type] @@ -113,6 +116,7 @@ class QAsyncioTask(futures.QAsyncioFuture): except BaseException as e: self._state = futures.QAsyncioFuture.FutureState.DONE_WITH_EXCEPTION self._exception = e + self._tb = traceback.format_exc() else: if asyncio.futures.isfuture(result): # If the coroutine yields a future, the task will await its @@ -159,7 +163,8 @@ class QAsyncioTask(futures.QAsyncioFuture): "task": self, "future": (exception_or_future if asyncio.futures.isfuture(exception_or_future) - else None) + else None), + "traceback": self._tb }) if self.done(): diff --git a/sources/pyside6/PySide6/QtCore/CMakeLists.txt b/sources/pyside6/PySide6/QtCore/CMakeLists.txt index d985b977..d559f9d9 100644 --- a/sources/pyside6/PySide6/QtCore/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtCore/CMakeLists.txt @@ -203,6 +203,7 @@ ${QtCore_GEN_DIR}/qxmlstreamnamespacedeclaration_wrapper.cpp ${QtCore_GEN_DIR}/qxmlstreamnotationdeclaration_wrapper.cpp ${QtCore_GEN_DIR}/qxmlstreamreader_wrapper.cpp ${QtCore_GEN_DIR}/qxmlstreamwriter_wrapper.cpp +${QtCore_GEN_DIR}/qmessagelogger_wrapper.cpp ${SPECIFIC_OS_FILES} # module is always needed diff --git a/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp b/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp index c073c8bc..c51d2274 100644 --- a/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp +++ b/sources/pyside6/PySide6/QtCore/glue/core_snippets.cpp @@ -45,7 +45,20 @@ QMetaType QVariant_resolveMetaType(PyTypeObject *type) // that has added any python fields or slots to its object layout. // See https://mail.python.org/pipermail/python-list/2009-January/520733.html if (type->tp_bases) { - for (Py_ssize_t i = 0, size = PyTuple_Size(type->tp_bases); i < size; ++i) { + const auto size = PyTuple_Size(type->tp_bases); + Py_ssize_t i = 0; + // PYSIDE-1887, PYSIDE-86: Skip QObject base class of QGraphicsObject; + // it needs to use always QGraphicsItem as a QVariant type for + // QGraphicsItem::itemChange() to work. + if (qstrcmp(typeName, "QGraphicsObject*") == 0 && size > 1) { + auto *firstBaseType = reinterpret_cast(PyTuple_GetItem(type->tp_bases, 0)); + if (SbkObjectType_Check(firstBaseType)) { + const char *firstBaseTypeName = Shiboken::ObjectType::getOriginalName(firstBaseType); + if (firstBaseTypeName != nullptr && qstrcmp(firstBaseTypeName, "QObject*") == 0) + ++i; + } + } + for ( ; i < size; ++i) { auto baseType = reinterpret_cast(PyTuple_GetItem(type->tp_bases, i)); const QMetaType derived = QVariant_resolveMetaType(baseType); if (derived.isValid()) diff --git a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml index cbcbbf8f..1544e6d4 100644 --- a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml +++ b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml @@ -21,16 +21,26 @@ - - + + + + + + + + + + + + - + - + @@ -150,7 +160,6 @@ - @@ -196,6 +205,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -562,7 +597,7 @@ - + @@ -578,7 +613,7 @@ - + @@ -592,14 +627,14 @@ - - - + + + - + @@ -616,7 +651,7 @@ - + @@ -636,14 +671,14 @@ - + - + @@ -652,7 +687,7 @@ - + @@ -700,8 +735,8 @@ - - + + + + @@ -1366,9 +1403,9 @@ - - - + + + @@ -1597,6 +1634,7 @@ + - - - + + + + + @@ -1849,16 +1889,20 @@ Like the method *findChild*, the first parameter should be the child's type. - + + + - + + + @@ -2260,9 +2304,8 @@ - - - + @@ -2418,11 +2461,11 @@ - - - + + @@ -2430,6 +2473,8 @@ + @@ -2492,6 +2537,7 @@ + @@ -2644,11 +2690,12 @@ + - + @@ -2773,7 +2820,7 @@ - + @@ -3017,7 +3064,7 @@ - + @@ -3084,7 +3131,7 @@ - + @@ -3306,25 +3353,25 @@ - + - + - + - + - + - + - + @@ -3368,13 +3415,13 @@ - + - + @@ -3407,7 +3454,7 @@ - + @@ -3447,23 +3494,23 @@ - + - + - + - + - + - + - + @@ -3531,7 +3578,7 @@ - + @@ -3591,6 +3638,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt b/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt index b32c39f8..29a7b254 100644 --- a/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtGraphs/CMakeLists.txt @@ -34,6 +34,7 @@ ${QtGraphs_GEN_DIR}/qlegenddata_wrapper.cpp ${QtGraphs_GEN_DIR}/qscatter3dseries_wrapper.cpp ${QtGraphs_GEN_DIR}/qscatterdataitem_wrapper.cpp ${QtGraphs_GEN_DIR}/qscatterdataproxy_wrapper.cpp +${QtGraphs_GEN_DIR}/qspline3dseries_wrapper.cpp ${QtGraphs_GEN_DIR}/qsurface3dseries_wrapper.cpp ${QtGraphs_GEN_DIR}/qsurfacedataitem_wrapper.cpp ${QtGraphs_GEN_DIR}/qsurfacedataproxy_wrapper.cpp diff --git a/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml b/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml index d929bcc0..81485a70 100644 --- a/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml +++ b/sources/pyside6/PySide6/QtGraphs/typesystem_graphs.xml @@ -12,8 +12,6 @@ - - @@ -23,6 +21,7 @@ + @@ -107,6 +106,7 @@ + diff --git a/sources/pyside6/PySide6/QtGui/CMakeLists.txt b/sources/pyside6/PySide6/QtGui/CMakeLists.txt index 94007538..e3207856 100644 --- a/sources/pyside6/PySide6/QtGui/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtGui/CMakeLists.txt @@ -138,6 +138,7 @@ ${QtGui_GEN_DIR}/qfontdatabase_wrapper.cpp ${QtGui_GEN_DIR}/qfontinfo_wrapper.cpp ${QtGui_GEN_DIR}/qfontmetrics_wrapper.cpp ${QtGui_GEN_DIR}/qfontmetricsf_wrapper.cpp +${QtGui_GEN_DIR}/qfontvariableaxis_wrapper.cpp ${QtGui_GEN_DIR}/qglyphrun_wrapper.cpp ${QtGui_GEN_DIR}/qgradient_wrapper.cpp ${QtGui_GEN_DIR}/qguiapplication_wrapper.cpp @@ -191,6 +192,7 @@ ${QtGui_GEN_DIR}/qpainter_wrapper.cpp ${QtGui_GEN_DIR}/qpainterpath_element_wrapper.cpp ${QtGui_GEN_DIR}/qpainterpath_wrapper.cpp ${QtGui_GEN_DIR}/qpainterpathstroker_wrapper.cpp +${QtGui_GEN_DIR}/qpainterstateguard_wrapper.cpp ${QtGui_GEN_DIR}/qpaintevent_wrapper.cpp ${QtGui_GEN_DIR}/qpalette_wrapper.cpp ${QtGui_GEN_DIR}/qpdfoutputintent_wrapper.cpp diff --git a/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml b/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml index e5637cf2..5cf69940 100644 --- a/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml +++ b/sources/pyside6/PySide6/QtGui/typesystem_gui_common.xml @@ -8,7 +8,6 @@ - @@ -157,7 +156,7 @@ - + @@ -435,7 +434,7 @@ - + @@ -517,9 +516,8 @@ - - - + @@ -628,11 +626,28 @@ - + - + + + + + + + + + + + + @@ -642,6 +657,7 @@ + @@ -717,22 +733,21 @@ - - - + - + - + @@ -789,14 +804,12 @@ - - - + - - - + @@ -871,7 +884,7 @@ - + @@ -894,7 +907,7 @@ - + @@ -1040,18 +1053,16 @@ - - - + - - - + @@ -1076,18 +1087,16 @@ - - - + - - - + @@ -1150,7 +1159,7 @@ - + @@ -1815,7 +1824,7 @@ - + @@ -1839,7 +1848,7 @@ - + @@ -1850,6 +1859,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1873,9 +2067,9 @@ - + - + @@ -1895,12 +2089,7 @@ - - - - - - + @@ -2016,8 +2205,18 @@ + + + + + + + + + + - + @@ -2054,7 +2253,7 @@ - + @@ -2091,7 +2290,7 @@ - + @@ -2128,7 +2327,7 @@ - + @@ -2165,7 +2364,7 @@ - + @@ -2202,7 +2401,7 @@ - + @@ -2239,7 +2438,7 @@ - + @@ -2276,7 +2475,7 @@ - + @@ -2313,7 +2512,7 @@ - + - + @@ -2449,7 +2648,7 @@ - + @@ -2462,7 +2661,7 @@ - + @@ -2491,7 +2690,7 @@ - + @@ -2523,7 +2722,7 @@ - + @@ -2601,9 +2800,8 @@ - - - + @@ -2643,6 +2841,9 @@ + + + @@ -2650,6 +2851,9 @@ + + + @@ -3048,9 +3252,8 @@ - - - + diff --git a/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml b/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml index 8a8eb5e2..18082888 100644 --- a/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml +++ b/sources/pyside6/PySide6/QtMultimedia/typesystem_multimedia.xml @@ -109,7 +109,8 @@ - + + diff --git a/sources/pyside6/PySide6/QtNetwork/typesystem_network.xml b/sources/pyside6/PySide6/QtNetwork/typesystem_network.xml index 9f727547..0f545312 100644 --- a/sources/pyside6/PySide6/QtNetwork/typesystem_network.xml +++ b/sources/pyside6/PySide6/QtNetwork/typesystem_network.xml @@ -48,7 +48,7 @@ - + @@ -277,13 +277,13 @@ - + - + diff --git a/sources/pyside6/PySide6/QtNetworkAuth/CMakeLists.txt b/sources/pyside6/PySide6/QtNetworkAuth/CMakeLists.txt index f713ff10..97a2e2bc 100644 --- a/sources/pyside6/PySide6/QtNetworkAuth/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtNetworkAuth/CMakeLists.txt @@ -10,6 +10,7 @@ ${QtNetworkAuth_GEN_DIR}/qabstractoauthreplyhandler_wrapper.cpp ${QtNetworkAuth_GEN_DIR}/qoauth1_wrapper.cpp ${QtNetworkAuth_GEN_DIR}/qoauth1signature_wrapper.cpp ${QtNetworkAuth_GEN_DIR}/qoauth2authorizationcodeflow_wrapper.cpp +${QtNetworkAuth_GEN_DIR}/qoauth2deviceauthorizationflow_wrapper.cpp ${QtNetworkAuth_GEN_DIR}/qoauthhttpserverreplyhandler_wrapper.cpp ${QtNetworkAuth_GEN_DIR}/qoauthoobreplyhandler_wrapper.cpp ${QtNetworkAuth_GEN_DIR}/qoauthurischemereplyhandler_wrapper.cpp diff --git a/sources/pyside6/PySide6/QtNetworkAuth/typesystem_networkauth.xml b/sources/pyside6/PySide6/QtNetworkAuth/typesystem_networkauth.xml index 888f3d43..cf99acaf 100644 --- a/sources/pyside6/PySide6/QtNetworkAuth/typesystem_networkauth.xml +++ b/sources/pyside6/PySide6/QtNetworkAuth/typesystem_networkauth.xml @@ -24,6 +24,7 @@ + @@ -70,6 +71,7 @@ + diff --git a/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl.xml b/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl.xml index efbd1605..f6afdf83 100644 --- a/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl.xml +++ b/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl.xml @@ -7,7 +7,6 @@ namespace-begin="QT_BEGIN_NAMESPACE" namespace-end="QT_END_NAMESPACE"> - diff --git a/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl_modifications1_0.xml b/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl_modifications1_0.xml index 152efad9..ef6c9a0e 100644 --- a/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl_modifications1_0.xml +++ b/sources/pyside6/PySide6/QtOpenGL/typesystem_opengl_modifications1_0.xml @@ -6,9 +6,7 @@ - - - + diff --git a/sources/pyside6/PySide6/QtPrintSupport/typesystem_printsupport_common.xml b/sources/pyside6/PySide6/QtPrintSupport/typesystem_printsupport_common.xml index aa64abed..4f31cf76 100644 --- a/sources/pyside6/PySide6/QtPrintSupport/typesystem_printsupport_common.xml +++ b/sources/pyside6/PySide6/QtPrintSupport/typesystem_printsupport_common.xml @@ -28,8 +28,6 @@ - - diff --git a/sources/pyside6/PySide6/QtQml/typesystem_qml.xml b/sources/pyside6/PySide6/QtQml/typesystem_qml.xml index 8477ac39..96bb81ed 100644 --- a/sources/pyside6/PySide6/QtQml/typesystem_qml.xml +++ b/sources/pyside6/PySide6/QtQml/typesystem_qml.xml @@ -342,4 +342,5 @@ + diff --git a/sources/pyside6/PySide6/QtQuick/typesystem_quick.xml b/sources/pyside6/PySide6/QtQuick/typesystem_quick.xml index 853c1589..87a6c51c 100644 --- a/sources/pyside6/PySide6/QtQuick/typesystem_quick.xml +++ b/sources/pyside6/PySide6/QtQuick/typesystem_quick.xml @@ -250,5 +250,6 @@ + diff --git a/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt b/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt index 07835b2f..2522ab54 100644 --- a/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt @@ -29,20 +29,23 @@ ${QtRemoteObjects_GEN_DIR}/qtroserveriodevice_wrapper.cpp ${QtRemoteObjects_GEN_DIR}/qtremoteobjects_module_wrapper.cpp ) +find_package(Qt6 REQUIRED COMPONENTS Core) + set(QtRemoteObjects_include_dirs ${QtRemoteObjects_SOURCE_DIR} ${QtRemoteObjects_BINARY_DIR} ${Qt${QT_MAJOR_VERSION}RemoteObjects_INCLUDE_DIRS} + ${libpysideremoteobjects_SOURCE_DIR} ${SHIBOKEN_INCLUDE_DIR} ${libpyside_SOURCE_DIR} ${SHIBOKEN_PYTHON_INCLUDE_DIR} ${QtCore_GEN_DIR} ${QtNetwork_GEN_DIR}) -set(QtRemoteObjects_libraries pyside6 - ${Qt${QT_MAJOR_VERSION}RemoteObjects_LIBRARIES}) - set(QtRemoteObjects_deps QtCore QtNetwork) +set(QtRemoteObjects_libraries pyside6 pyside6remoteobjects + ${Qt${QT_MAJOR_VERSION}RemoteObjects_LIBRARIES}) + create_pyside_module(NAME QtRemoteObjects INCLUDE_DIRS QtRemoteObjects_include_dirs LIBRARIES QtRemoteObjects_libraries diff --git a/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml b/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml index 86e4d909..a6e54ee1 100644 --- a/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml +++ b/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml @@ -8,6 +8,9 @@ + + #include "pysideremoteobjects.h" + @@ -26,6 +29,10 @@ + + + @@ -35,7 +42,12 @@ - + + + + + + @@ -53,4 +65,7 @@ + + diff --git a/sources/pyside6/PySide6/QtSerialBus/typesystem_serialbus.xml b/sources/pyside6/PySide6/QtSerialBus/typesystem_serialbus.xml index 6cf562c5..8326b1a5 100644 --- a/sources/pyside6/PySide6/QtSerialBus/typesystem_serialbus.xml +++ b/sources/pyside6/PySide6/QtSerialBus/typesystem_serialbus.xml @@ -86,6 +86,8 @@ + + diff --git a/sources/pyside6/PySide6/QtTest/typesystem_test.xml b/sources/pyside6/PySide6/QtTest/typesystem_test.xml index 173392f3..dbf4ef6c 100644 --- a/sources/pyside6/PySide6/QtTest/typesystem_test.xml +++ b/sources/pyside6/PySide6/QtTest/typesystem_test.xml @@ -66,18 +66,18 @@ - + - + - + - + @@ -114,12 +114,12 @@ - + - + diff --git a/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt b/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt index 9cc3d87e..7afb9ef1 100644 --- a/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtWebEngineCore/CMakeLists.txt @@ -37,6 +37,7 @@ ${QtWebEngineCore_GEN_DIR}/qwebenginenotification_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginepage_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginepermission_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebengineprofile_wrapper.cpp +${QtWebEngineCore_GEN_DIR}/qwebengineprofilebuilder_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginequotarequest_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebengineregisterprotocolhandlerrequest_wrapper.cpp ${QtWebEngineCore_GEN_DIR}/qwebenginescript_wrapper.cpp diff --git a/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml b/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml index 36c15c4e..003c18de 100644 --- a/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml +++ b/sources/pyside6/PySide6/QtWebEngineCore/typesystem_webenginecore.xml @@ -202,6 +202,8 @@ + + diff --git a/sources/pyside6/PySide6/QtWebEngineQuick/CMakeLists.txt b/sources/pyside6/PySide6/QtWebEngineQuick/CMakeLists.txt index d87dc55a..77152e9f 100644 --- a/sources/pyside6/PySide6/QtWebEngineQuick/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtWebEngineQuick/CMakeLists.txt @@ -4,6 +4,7 @@ project(QtWebEngineQuick) set(QtWebEngineQuick_SRC +${QtWebEngineQuick_GEN_DIR}/qquickwebenginedownloadrequest_wrapper.cpp ${QtWebEngineQuick_GEN_DIR}/qquickwebengineprofile_wrapper.cpp ${QtWebEngineQuick_GEN_DIR}/qtwebenginequick_wrapper.cpp # module is always needed diff --git a/sources/pyside6/PySide6/QtWebEngineQuick/typesystem_webenginequick.xml b/sources/pyside6/PySide6/QtWebEngineQuick/typesystem_webenginequick.xml index 3d383337..ae6b4122 100644 --- a/sources/pyside6/PySide6/QtWebEngineQuick/typesystem_webenginequick.xml +++ b/sources/pyside6/PySide6/QtWebEngineQuick/typesystem_webenginequick.xml @@ -15,5 +15,7 @@ + + diff --git a/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml b/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml index 7879cdeb..f1b9e14d 100644 --- a/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml +++ b/sources/pyside6/PySide6/QtWidgets/typesystem_widgets_common.xml @@ -30,8 +30,6 @@ enum 'QGraphicsPolygonItem::Type' does not have a type entry or is not an enum """ --> - - @@ -260,7 +258,7 @@ - + @@ -329,7 +327,7 @@ - + @@ -419,12 +417,12 @@ - + - + @@ -739,7 +737,7 @@ - + @@ -1123,7 +1121,7 @@ polymorphic-id-expression="%B->type() == QEvent::GraphicsSceneWheel"/> + polymorphic-id-expression="%B->type() == QEvent::Gesture || %B->type() == QEvent::GestureOverride"> @@ -1161,7 +1159,7 @@ - + @@ -1460,11 +1458,16 @@ - + + + - + + + + @@ -1486,6 +1489,7 @@ + @@ -1497,6 +1501,7 @@ + @@ -1508,6 +1513,7 @@ + @@ -1519,6 +1525,7 @@ + @@ -1530,6 +1537,7 @@ + @@ -1724,7 +1732,7 @@ - + @@ -1954,7 +1962,7 @@ - + @@ -1999,9 +2007,8 @@ - - - + @@ -2315,18 +2322,34 @@ snippet="qmessagebox-open-connect-accept"/> - + + + - + allow-thread="yes"> + + + + + - + allow-thread="yes"> + + + + + - + allow-thread="yes"> + + + + + + allow-thread="yes"> + + @@ -2336,8 +2359,12 @@ - - + + + + + + @@ -2353,6 +2380,9 @@ + + + @@ -2433,7 +2463,7 @@ - + @@ -2553,6 +2583,11 @@ + + + + + @@ -2732,6 +2767,9 @@ + + + @@ -2828,6 +2866,7 @@ + @@ -3088,33 +3127,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -3203,29 +3215,6 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -3343,10 +3332,10 @@ - + - + @@ -3359,28 +3348,28 @@ - - - - - + + + + + - - + + - + - - - - - - - + + + + + + + @@ -3389,11 +3378,11 @@ - + - + diff --git a/sources/pyside6/PySide6/__init__.py.in b/sources/pyside6/PySide6/__init__.py.in index 45c19f2e..197eba96 100644 --- a/sources/pyside6/PySide6/__init__.py.in +++ b/sources/pyside6/PySide6/__init__.py.in @@ -101,10 +101,13 @@ def _find_all_qt_modules(): # Instead, we use __getattr__ which is supported since Python 3.7 # and create the __all__ list on demand when needed. - location = Path(__file__).resolve().parent - files = os.listdir(location) - unordered = set(name[: name.find(".")] for name in files if name.startswith("Qt") and ( - name.endswith((".pyd", ".so")))) + unordered = set() + pattern = "Qt*.pyd" if sys.platform == "win32" else "Qt*.so" + for module in Path(__file__).resolve().parent.glob(pattern): + name = module.name[:module.name.find(".")] + if name.endswith("_d"): # Windows debug suffix? + name = name[:-2] + unordered.add(name) ordered_part = __pre_all__ result = [] for name in ordered_part: diff --git a/sources/pyside6/PySide6/_config.py.in b/sources/pyside6/PySide6/_config.py.in index 27ee0789..34bff7e5 100644 --- a/sources/pyside6/PySide6/_config.py.in +++ b/sources/pyside6/PySide6/_config.py.in @@ -2,8 +2,8 @@ built_modules = list(name for name in "@all_module_shortnames@" .split(";")) -shiboken_library_soversion = str(@SHIBOKEN_SO_VERSION@) -pyside_library_soversion = str(@PYSIDE_SO_VERSION@) +shiboken_library_soversion = "@SHIBOKEN_SO_VERSION@" +pyside_library_soversion = "@PYSIDE_SO_VERSION@" version = "@FINAL_PACKAGE_VERSION@" version_info = (@BINDING_API_MAJOR_VERSION@, @BINDING_API_MINOR_VERSION@, @BINDING_API_MICRO_VERSION@, "@BINDING_API_PRE_RELEASE_VERSION_TYPE@", "@BINDING_API_PRE_RELEASE_VERSION@") diff --git a/sources/pyside6/PySide6/doc/qtcore.rst b/sources/pyside6/PySide6/doc/qtcore.rst index 35ba0d1c..b8d551e7 100644 --- a/sources/pyside6/PySide6/doc/qtcore.rst +++ b/sources/pyside6/PySide6/doc/qtcore.rst @@ -93,3 +93,26 @@ to a list. settings.value('var', type=list) # Will get ["a"] // @snippet qsettings-value + +// @snippet qmessagelogger + +In Python, the :class:`QMessageLogger` is useful to connect an existing logging +setup that uses the Python logging module to the Qt logging system. This allows +you to leverage Qt's logging infrastructure while still using the familiar +Python logging API. + +Example:: + + import logging + from PySide6.QtCore import QMessageLogger + + class LogHandler(logging.Handler): + def emit(self, record: logging.LogRecord): + if record.levelno == logging.DEBUG: + logger = QMessageLogger(record.filename, record.lineno, record.funcName) + logger.debug(record.message) + + logging.basicConfig(handlers=[LogHandler()]) + logging.debug("Test debug message") + +// @snippet qmessagelogger diff --git a/sources/pyside6/PySide6/doc/qtqml.rst b/sources/pyside6/PySide6/doc/qtqml.rst index 31801b24..ef171f80 100644 --- a/sources/pyside6/PySide6/doc/qtqml.rst +++ b/sources/pyside6/PySide6/doc/qtqml.rst @@ -139,10 +139,10 @@ It is recommended to store the QML type id, e.g. as a static member in the singleton class. The lookup via qmlTypeId() is costly. // @snippet qqmlengine-singletoninstance-qmltypeid -// @snippet qqmlengine-singletoninstance-typename Returns the instance of a -singleton type named typeName from the module specified by uri. -For ``QObject``-derived singleton types, the ``QObject`` instance is returned, -otherwise a ``QJSValue`` or ``None``. +// @snippet qqmlengine-singletoninstance-typename +Returns the instance of a singleton type named typeName from the module specified +by uri. For ``QObject``-derived singleton types, the ``QObject`` instance is +returned, otherwise a ``QJSValue`` or ``None``. This method can be used as an alternative to calling qmlTypeId followed by the id based overload of singletonInstance. This is convenient when one only needs diff --git a/sources/pyside6/PySide6/glue/qtcore.cpp b/sources/pyside6/PySide6/glue/qtcore.cpp index c3465682..0a5019bf 100644 --- a/sources/pyside6/PySide6/glue/qtcore.cpp +++ b/sources/pyside6/PySide6/glue/qtcore.cpp @@ -100,7 +100,7 @@ static PyObject *settingsTypeCoercion(const QVariant &out, PyTypeObject *typeObj // Convert any string, etc, to a list of 1 element if (auto *primitiveValue = convertToPrimitiveType(out, out.typeId())) { PyObject *list = PyList_New(1); - PyList_SET_ITEM(list, 0, primitiveValue); + PyList_SetItem(list, 0, primitiveValue); return list; } @@ -113,7 +113,7 @@ static PyObject *settingsTypeCoercion(const QVariant &out, PyTypeObject *typeObj PyObject *list = PyList_New(valuesSize); for (Py_ssize_t i = 0; i < valuesSize; ++i) { PyObject *item = PyUnicode_FromString(valuesList.at(i).constData()); - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); } return list; } @@ -433,6 +433,10 @@ static PyObject *qtmsghandler = nullptr; static void msgHandlerCallback(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) { Shiboken::GilState state; + PyObject *excType{}; + PyObject *excValue{}; + PyObject *excTraceback{}; + PyErr_Fetch(&excType, &excValue, &excTraceback); Shiboken::AutoDecRef arglist(PyTuple_New(3)); PyTuple_SetItem(arglist, 0, %CONVERTTOPYTHON[QtMsgType](type)); PyTuple_SetItem(arglist, 1, %CONVERTTOPYTHON[QMessageLogContext &](ctx)); @@ -440,6 +444,7 @@ static void msgHandlerCallback(QtMsgType type, const QMessageLogContext &ctx, co const char *data = array.constData(); PyTuple_SetItem(arglist, 2, %CONVERTTOPYTHON[const char *](data)); Shiboken::AutoDecRef ret(PyObject_CallObject(qtmsghandler, arglist)); + PyErr_Restore(excType, excValue, excTraceback); } // @snippet qt-messagehandler @@ -1292,6 +1297,18 @@ Py_BEGIN_ALLOW_THREADS Py_END_ALLOW_THREADS // @snippet qdebug-format-string +// @snippet qmessagelogger-format-string +Py_BEGIN_ALLOW_THREADS +%CPPSELF->%FUNCTION_NAME("%s", %1); // Uses placeholder for security reasons +Py_END_ALLOW_THREADS +// @snippet qmessagelogger-format-string + +// @snippet qmessagelogger-logcategory-format-string +Py_BEGIN_ALLOW_THREADS +%CPPSELF->%FUNCTION_NAME(%1, "%s", %2); // Uses placeholder for security reasons +Py_END_ALLOW_THREADS +// @snippet qmessagelogger-logcategory-format-string + // @snippet qresource-registerResource auto ptr = reinterpret_cast(Shiboken::Buffer::getPointer(%PYARG_1)); %RETURN_TYPE %0 = %CPPSELF.%FUNCTION_NAME(const_cast(ptr), %2); @@ -1609,6 +1626,14 @@ return PyBool_FromLong((bool)%in); return PyBytes_FromStringAndSize(%in.constData(), %in.size()); // @snippet return-pybytes +// @snippet chrono-to-pylong +return PyLong_FromLong(%in.count()); +// @snippet chrono-to-pylong + +// @snippet pylong-to-chrono +%out = %OUTTYPE(PyLong_AsLongLong(%in)); +// @snippet pylong-to-chrono + // @snippet return-pylong return PyLong_FromLong(%in); // @snippet return-pylong @@ -1738,18 +1763,51 @@ pthread_cleanup_pop(0); #endif // @snippet qthread_pthread_cleanup_uninstall -// @snippet qlibraryinfo_build -auto oldResult = pyResult; -const auto version = _PepRuntimeVersion(); -pyResult = PyUnicode_FromFormat( -#ifdef Py_LIMITED_API - "%U [Python limited API %d.%d.%d]", +// @snippet qlibraryinfo_python_build + +// For versions with one byte per digit. +static QByteArray versionString(long version) +{ + return QByteArray::number((version >> 16) & 0xFF) + + '.' + QByteArray::number((version >> 8) & 0xFF) + + '.' + QByteArray::number(version & 0xFF); +} + +static QByteArray pythonBuild() +{ + using namespace Qt::StringLiterals; + +#ifdef PYPY_VERSION + QByteArray result = "PyPy " PYPY_VERSION #else - "%U [Python %d.%d.%d]", + QByteArray result = "Python" +#endif +#ifdef Py_LIMITED_API + " limited API" +#endif +#ifdef Py_GIL_DISABLED + " free threaded" #endif - oldResult, (version >> 16) & 0xFF, - (version >> 8) & 0xFF, version & 0xFF); -Py_DECREF(oldResult); + ; + result += ' '; + + const auto runTimeVersion = _PepRuntimeVersion(); + const auto runTimeVersionB = versionString(runTimeVersion); + constexpr long buildVersion = PY_VERSION_HEX >> 8; + if (runTimeVersion == buildVersion) { + result += runTimeVersionB; + } else { + result += "run time: "_ba + runTimeVersionB + " built: "_ba + + versionString(buildVersion); + } + return result; +} +// @snippet qlibraryinfo_python_build + +// @snippet qlibraryinfo_build +QByteArray %0 = %CPPSELF.%FUNCTION_NAME(); +%0 += " [" + pythonBuild() + ']'; +%PYARG_0 = PyUnicode_FromString(%0.constData()); // @snippet qlibraryinfo_build // @snippet qsharedmemory_data_readonly @@ -2167,3 +2225,21 @@ QByteArray result = '<' + QByteArray(Py_TYPE(%PYSELF)->tp_name) + %CPPSELF.absoluteFilePath().toUtf8() + "\")>"; %PYARG_0 = Shiboken::String::fromCString(result.constData()); // @snippet qdirlisting-direntry-repr + +// @snippet return-native-eventfilter-conversion +%RETURN_TYPE %out = false; +if (PySequence_Check(%PYARG_0) != 0 && PySequence_Size(%PYARG_0) == 2) { + Shiboken::AutoDecRef pyItem(PySequence_GetItem(%PYARG_0, 0)); + %out = %CONVERTTOCPP[bool](pyItem); + if (result) { + Shiboken::AutoDecRef pyResultItem(PySequence_GetItem(pyResult, 1)); + *result = %CONVERTTOCPP[qintptr](pyResultItem); + } +} +// @snippet return-native-eventfilter-conversion + +// @snippet return-native-eventfilter +%PYARG_0 = PyTuple_New(2); +PyTuple_SetItem(%PYARG_0, 0, %CONVERTTOPYTHON[%RETURN_TYPE](%0)); +PyTuple_SetItem(%PYARG_0, 1, %CONVERTTOPYTHON[qintptr](*result_out)); +// @snippet return-native-eventfilter diff --git a/sources/pyside6/PySide6/glue/qtgui.cpp b/sources/pyside6/PySide6/glue/qtgui.cpp index a91478c5..72d3d2b4 100644 --- a/sources/pyside6/PySide6/glue/qtgui.cpp +++ b/sources/pyside6/PySide6/glue/qtgui.cpp @@ -355,7 +355,7 @@ for (Py_ssize_t i = 0; i < count; ++i){ int x, y; %CPPSELF.point(i, &x, &y); QPoint pt{x, y}; - PyList_SET_ITEM(points, i, %CONVERTTOPYTHON[QPoint](pt)); + PyList_SetItem(points, i, %CONVERTTOPYTHON[QPoint](pt)); } // @snippet qpolygon-reduce @@ -369,6 +369,29 @@ for (Py_ssize_t i = 0; i < count; ++i){ %0 = new %TYPE(QPixmap::fromImage(%1)); // @snippet qpixmap +// @snippet qpixmap-load-xpm +Shiboken::AutoDecRef strList(PySequence_Fast(%PYARG_1, "Invalid sequence.")); +Py_ssize_t lineCount = PySequence_Size(strList.object()); +for (Py_ssize_t line = 0; line < lineCount; ++line) { + Shiboken::AutoDecRef _obj(PySequence_GetItem(strList.object(), line)); + if (!Shiboken::String::check(_obj)) { + PyErr_SetString(PyExc_TypeError, "The argument must be a sequence of strings."); + break; + } +} +// PySIDE-1735: Enums are now implemented in Python, so we need to avoid asserts. +if (PyErr_Occurred()) + break; + +Shiboken::ArrayPointer xpm(lineCount); +for (Py_ssize_t line = 0; line < lineCount; ++line) { + Shiboken::AutoDecRef _obj(PySequence_GetItem(strList.object(), line)); + xpm[line] = Shiboken::String::toCString(_obj); +} + +%0 = new %TYPE(xpm); +// @snippet qpixmap-load-xpm + // @snippet qicon-addpixmap const auto path = PySide::pyPathToQString(%PYARG_1); %CPPSELF->addPixmap(path); @@ -384,6 +407,12 @@ const auto path = PySide::pyPathToQString(%PYARG_1); %CPPSELF->setImage(QImage(path)); // @snippet qclipboard-setimage +// @snippet qimage-buffer-constructor +Py_INCREF(%PYARG_1); +auto *ptr = reinterpret_cast(Shiboken::Buffer::getPointer(%PYARG_1)); +%0 = new %TYPE(ptr, %ARGS, imageDecrefDataHandler, %PYARG_1); +// @snippet qimage-buffer-constructor + // @snippet qimage-decref-image-data static void imageDecrefDataHandler(void *data) { @@ -495,6 +524,44 @@ switch (%CPPSELF.spec()) { } // @snippet qcolor-totuple +// @snippet qcolor-repr +QString repr; +switch (%CPPSELF.spec()) { +case QColor::Rgb: { + float r, g, b, a; + %CPPSELF.getRgbF(&r, &g, &b, &a); + repr = QString::asprintf("PySide6.QtGui.QColor.fromRgbF(%.6f, %.6f, %.6f, %.6f)", + r, g, b, a); + break; +} +case QColor::Hsv: { + float h, s, v, a; + %CPPSELF.getHsvF(&h, &s, &v, &a); + repr = QString::asprintf("PySide6.QtGui.QColor.fromHsvF(%.6f, %.6f, %.6f, %.6f)", + h, s, v, a); + break; +} +case QColor::Cmyk: { + float c, m, y, k, a; + %CPPSELF.getCmykF(&c, &m, &y, &k, &a); + repr = QString::asprintf("PySide6.QtGui.QColor.fromCmykF(%.6f, %.6f, %.6f, %.6f, %.6f)", + c, m, y, k, a); + break; +} +case QColor::Hsl: { + float h, s, l, a; + %CPPSELF.getHslF(&h, &s, &l, &a); + repr = QString::asprintf("PySide6.QtGui.QColor.fromHslF(%.6f, %.6f, %.6f, %.6f)", + h, s, l, a); + break; +} +default: + repr = QLatin1StringView("PySide6.QtGui.QColor()"); + break; +} +%PYARG_0 = Shiboken::String::fromCString(qPrintable(repr)); +// @snippet qcolor-repr + // @snippet qcolor if (%1.type() == QVariant::Color) %0 = new %TYPE(%1.value()); @@ -502,6 +569,55 @@ else PyErr_SetString(PyExc_TypeError, "QVariant must be holding a QColor"); // @snippet qcolor +// @snippet qfont-tag-from-str-helper +using FontTagOptional = std::optional; +static std::optional qFontTagFromString(PyObject *unicode) +{ + FontTagOptional result; + if (PyUnicode_GetLength(unicode) == 4) + result = QFont::Tag::fromString(PySide::pyUnicodeToQString(unicode)); + if (!result.has_value()) + PyErr_SetString(PyExc_TypeError, + "QFont::Tag(): The tag name must be exactly 4 characters long."); + return result; +} +// @snippet qfont-tag-from-str-helper + +// @snippet qfont-tag-init-str +const FontTagOptional tagO = qFontTagFromString(%PYARG_1); +if (tagO.has_value()) + %0 = new QFont::Tag(tagO.value()); +// @snippet qfont-tag-init-str + +// @snippet qfont-tag-fromString +const FontTagOptional tagO = qFontTagFromString(%PYARG_1); +if (tagO.has_value()) { + const auto &tag = tagO.value(); + %PYARG_0 = %CONVERTTOPYTHON[%RETURN_TYPE](tag); +} +// @snippet qfont-tag-fromString + +// @snippet qfont-tag-fromValue +const FontTagOptional tagO = QFont::Tag::fromValue(PyLong_AsUnsignedLong(%PYARG_1)); +if (tagO.has_value()) { + const auto &tag = tagO.value(); + %PYARG_0 = %CONVERTTOPYTHON[%RETURN_TYPE](tag); +} else { + PyErr_SetString(PyExc_TypeError, "QFont::Tag::fromValue(): Invalid value passed."); +} +// @snippet qfont-tag-fromValue + +// @snippet qfontmetrics-qfontcharfix +if (Shiboken::String::len(%PYARG_1) == 1) { + const char *str = Shiboken::String::toCString(%PYARG_1); + const QChar ch(static_cast(str[0])); + %RETURN_TYPE %0 = %CPPSELF.%FUNCTION_NAME(ch); + %PYARG_0 = %CONVERTTOPYTHON[%RETURN_TYPE](%0); +} else { + PyErr_SetString(PyExc_TypeError, "String must have only one character"); +} +// @snippet qfontmetrics-qfontcharfix + // @snippet qfontmetricsf-boundingrect int *array = nullptr; bool errorOccurred = false; @@ -934,6 +1050,122 @@ return %CONVERTTOPYTHON[QRect](cppResult); %CPPSELF.%FUNCTION_NAME(%1, %2.size(), %2.constData(), %3, %4, %5); // @snippet qrhi-commandbuffer-setvertexinput +// @snippet qpainterstateguard-restore +%CPPSELF.restore(); +// @snippet qpainterstateguard-restore + +// @snippet qmatrix-repr-code +QByteArray format(Py_TYPE(%PYSELF)->tp_name); +format += QByteArrayLiteral("(("); +%MATRIX_TYPE data[%MATRIX_SIZE]; +%CPPSELF.copyDataTo(data); +for (int i = 0; i < %MATRIX_SIZE; ++i) { + if (i > 0) + format += ", "; + format += QByteArray::number(data[i]); +} +format += "))"; + +%PYARG_0 = Shiboken::String::fromStringAndSize(format, format.size()); +// @snippet qmatrix-repr-code + +// @snippet qmatrix-reduce-code +%MATRIX_TYPE data[%MATRIX_SIZE]; +%CPPSELF.copyDataTo(data); +QList<%MATRIX_TYPE> cppArgs(data, data + %MATRIX_SIZE); +PyObject *type = PyObject_Type(%PYSELF); +PyObject *args = Py_BuildValue("(N)", + %CONVERTTOPYTHON[QList<%MATRIX_TYPE>](cppArgs)); +%PYARG_0 = Py_BuildValue("(NN)", type, args); +// @snippet qmatrix-reduce-code + +// @snippet qmatrix-data-function +PyObject *pyData = PyTuple_New(%MATRIX_SIZE); +if (const float *data = %CPPSELF.constData()) { + for (int i = 0; i < %MATRIX_SIZE; ++i) + PyTuple_SetItem(pyData, i, %CONVERTTOPYTHON[float](data[i])); +} +return pyData; +// @snippet qmatrix-data-function + +// @snippet qmatrix-constructor +// PYSIDE-795: All PySequences can be made iterable with PySequence_Fast. +Shiboken::AutoDecRef seq(PySequence_Fast(%PYARG_1, "Can't turn into sequence")); +if (PySequence_Size(seq) == %SIZE) { + Shiboken::AutoDecRef fast(PySequence_Fast(seq, + "Failed to parse sequence on %TYPE constructor.")); + float values[%SIZE]; + for (int i = 0; i < %SIZE; ++i) { + Shiboken::AutoDecRef pv(PySequence_GetItem(fast.object(), i)); + values[i] = %CONVERTTOCPP[float](pv); + } + %0 = new %TYPE(values); +} +// @snippet qmatrix-constructor + +// @snippet validator-conversionrule +QValidator::State %out; + +if (PySequence_Check(%PYARG_0)) { + Shiboken::AutoDecRef seq(PySequence_Fast(%PYARG_0, 0)); + const Py_ssize_t size = PySequence_Size(seq.object()); + + if (size > 1) { + Shiboken::AutoDecRef _obj1(PySequence_GetItem(seq.object(), 1)); + if (%ISCONVERTIBLE[QString](_obj1)) + %1 = %CONVERTTOCPP[QString](_obj1); + else + qWarning("%TYPE::%FUNCTION_NAME: Second tuple element is not convertible to unicode."); + } + + if (size > 2) { + Shiboken::AutoDecRef _obj2(PySequence_GetItem(seq.object(), 2)); + if (%ISCONVERTIBLE[int](_obj2)) + %2 = %CONVERTTOCPP[int](_obj2); + else + qWarning("%TYPE::%FUNCTION_NAME: Second tuple element is not convertible to int."); + } + Shiboken::AutoDecRef _sobj(PySequence_GetItem(seq.object(), 0)); + + %PYARG_0.reset(_sobj); + Py_INCREF(%PYARG_0); // we need to incref, because "%PYARG_0 = ..." will decref the tuple and the tuple will be decrefed again at the end of this scope. +} + +// check return value +if (%ISCONVERTIBLE[QValidator::State](%PYARG_0)) { + %out = %CONVERTTOCPP[QValidator::State](%PYARG_0); +} else { + PyErr_Format(PyExc_TypeError, "Invalid return value in function %s, expected %s, got %s.", + "QValidator.validate", + "PySide6.QtGui.QValidator.State, (PySide6.QtGui.QValidator.State,), (PySide6.QtGui.QValidator.State, unicode) or (PySide6.QtGui.QValidator.State, unicode, int)", + Py_TYPE(pyResult)->tp_name); + return QValidator::State(); +} +// @snippet validator-conversionrule + +// @snippet fix_margins_return +PyObject *obj = %PYARG_0.object(); +bool ok = false; +if (PySequence_Check(obj) != 0 && PySequence_Size(obj) == 4) { + Shiboken::AutoDecRef m0(PySequence_GetItem(obj, 0)); + Shiboken::AutoDecRef m1(PySequence_GetItem(obj, 1)); + Shiboken::AutoDecRef m2(PySequence_GetItem(obj, 2)); + Shiboken::AutoDecRef m3(PySequence_GetItem(obj, 3)); + ok = PyNumber_Check(m0) != 0 && PyNumber_Check(m1) != 0 + && PyNumber_Check(m2) && PyNumber_Check(m3) != 0; + if (ok) { + *%1 = %CONVERTTOCPP[$TYPE](m0); + *%2 = %CONVERTTOCPP[$TYPE](m1); + *%3 = %CONVERTTOCPP[$TYPE](m2); + *%4 = %CONVERTTOCPP[$TYPE](m3); + } +} +if (!ok) { + PyErr_SetString(PyExc_TypeError, "Sequence of 4 numbers expected"); + %1 = %2 = %3 = %4 = 0; +} +// @snippet fix_margins_return + /********************************************************************* * CONVERSIONS ********************************************************************/ diff --git a/sources/pyside6/PySide6/glue/qtnetwork.cpp b/sources/pyside6/PySide6/glue/qtnetwork.cpp index 7b1c2d56..9a4e28ce 100644 --- a/sources/pyside6/PySide6/glue/qtnetwork.cpp +++ b/sources/pyside6/PySide6/glue/qtnetwork.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only // @snippet qudpsocket-readdatagram -Shiboken::AutoArrayPointer data(%ARGUMENT_NAMES); +Shiboken::ArrayPointer data(%ARGUMENT_NAMES); QHostAddress ha; quint16 port; %BEGIN_ALLOW_THREADS diff --git a/sources/pyside6/PySide6/glue/qtquick.cpp b/sources/pyside6/PySide6/glue/qtquick.cpp index 060418fa..8691af07 100644 --- a/sources/pyside6/PySide6/glue/qtquick.cpp +++ b/sources/pyside6/PySide6/glue/qtquick.cpp @@ -11,7 +11,7 @@ const Py_ssize_t vertexCount = %CPPSELF->vertexCount(); %PYARG_0 = PyList_New(vertexCount); for (Py_ssize_t i = 0; i < vertexCount; ++i) { QSGGeometry::Point2D p = points[i]; - PyList_SET_ITEM(%PYARG_0, i, %CONVERTTOPYTHON[QSGGeometry::Point2D](p)); + PyList_SetItem(%PYARG_0, i, %CONVERTTOPYTHON[QSGGeometry::Point2D](p)); } // @snippet qsgeometry-vertexdataaspoint2d diff --git a/sources/pyside6/PySide6/glue/qtremoteobjects.cpp b/sources/pyside6/PySide6/glue/qtremoteobjects.cpp new file mode 100644 index 00000000..88d58589 --- /dev/null +++ b/sources/pyside6/PySide6/glue/qtremoteobjects.cpp @@ -0,0 +1,31 @@ +// Copyright (C) 2024 Ford Motor Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// @snippet qtro-init +PySide::RemoteObjects::init(module); +// @snippet qtro-init + +// @snippet node-acquire +auto *typeObject = reinterpret_cast(%PYARG_1); +if (!PySide::inherits(typeObject, SbkPySide6_QtRemoteObjectsTypeStructs[SBK_QRemoteObjectReplica_IDX].fullName)) { + PyErr_SetString(PyExc_TypeError, "First argument must be a type deriving from QRemoteObjectReplica."); + return nullptr; +} + +static PyObject *pyConstructWithNode = Shiboken::Enum::newItem( + Shiboken::Module::get(SbkPySide6_QtRemoteObjectsTypeStructs[SBK_QRemoteObjectReplica_ConstructorType_IDX]), + 1 /* protected QRemoteObjectReplica::ConstructorType::ConstructWithNode */ +); + +Shiboken::AutoDecRef args; +if (pyArgs[1]) + args.reset(PyTuple_Pack(3, %PYSELF, pyConstructWithNode, pyArgs[1])); +else + args.reset(PyTuple_Pack(2, %PYSELF, pyConstructWithNode)); + +PyObject *instance = PyObject_CallObject(%PYARG_1, args.object()); +if (!instance) + return nullptr; // Propagate the exception + +%PYARG_0 = instance; +// @snippet node-acquire diff --git a/sources/pyside6/PySide6/glue/qttest.cpp b/sources/pyside6/PySide6/glue/qttest.cpp index 36d3bfa3..6d6336a8 100644 --- a/sources/pyside6/PySide6/glue/qttest.cpp +++ b/sources/pyside6/PySide6/glue/qttest.cpp @@ -20,5 +20,11 @@ if (emitter == nullptr || signature.isEmpty()) { PyErr_SetString(PyExc_ValueError, error.constData()); return -1; } + +// PySide::Signal::getObject() increments the refcount for emitterPyObject, +// but there is nothing that decrements the count when the spy goes out of +// scope. It doesn't seem like QSignalSpy should prevent the target object +// from being garbage collected. So we need to decrement the refcount here. +Py_DECREF(emitterPyObject); %0 = new QSignalSpy(emitter, signature.constData()); // @snippet qsignalspy-signal diff --git a/sources/pyside6/PySide6/glue/qtwidgets.cpp b/sources/pyside6/PySide6/glue/qtwidgets.cpp index bc9dc99d..c085cb06 100644 --- a/sources/pyside6/PySide6/glue/qtwidgets.cpp +++ b/sources/pyside6/PySide6/glue/qtwidgets.cpp @@ -815,6 +815,16 @@ if (!PySide::callConnect(%PYSELF, SIGNAL(accepted()), %PYARG_1)) %CPPSELF.%FUNCTION_NAME(); // @snippet qmessagebox-open-connect-accept +// @snippet replace-widget-child +$CHILD_TYPE* oldChild = %CPPSELF.$FUNCTION_GET_OLD(); +if (oldChild != nullptr && oldChild != $CPPARG) { + Shiboken::AutoDecRef pyChild(%CONVERTTOPYTHON[$CHILD_TYPE*](oldChild)); + Shiboken::Object::setParent(nullptr, pyChild); + Shiboken::Object::releaseOwnership(pyChild); +} +Shiboken::Object::setParent(%PYSELF, $PYARG); +// @snippet replace-widget-child + /********************************************************************* * CONVERSIONS ********************************************************************/ diff --git a/sources/pyside6/PySide6/templates/common.xml b/sources/pyside6/PySide6/templates/common.xml index aece7eaa..663a0a5c 100644 --- a/sources/pyside6/PySide6/templates/common.xml +++ b/sources/pyside6/PySide6/templates/common.xml @@ -24,4 +24,28 @@ + + + + + + + + + diff --git a/sources/pyside6/PySide6/templates/core_common.xml b/sources/pyside6/PySide6/templates/core_common.xml index c794e7b8..5149f1a7 100644 --- a/sources/pyside6/PySide6/templates/core_common.xml +++ b/sources/pyside6/PySide6/templates/core_common.xml @@ -60,24 +60,6 @@ - - - - - - - - - - - - - + - - - +